Host ACP in Process

Applications that speak ACP can embed its session multiplexer instead of starting a caretz acp subprocess. An acp.Host manages multiple independently configured workspace runtimes. Each workspace starts lazily and carries any number of ACP sessions over one ordered message stream.

This complete example starts one workspace and sends initialize:

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"os"

	"github.com/makkalot/caretz/acp"
	"github.com/makkalot/caretz/cli"
)

func main() {
	ctx := context.Background()
	host := acp.NewHost(ctx)
	defer host.Close()

	err := host.RegisterWorkspace("project", acp.WorkspaceConfig{
		Runtime: func(runtimeCtx context.Context) (acp.Runtime, error) {
			runtime, code := cli.BuildRuntime(
				runtimeCtx,
				[]string{"--workspace", "/srv/project", "--model", "openai/gpt-5"},
				os.Stderr,
				cli.Options{},
			)
			if code != 0 || runtime == nil {
				return nil, fmt.Errorf("build Caretz runtime: exit code %d", code)
			}
			return runtime, nil
		},
		Server: acp.ServerOptions{
			AllowedModels: []string{"openai/gpt-5"},
		},
	})
	if err != nil {
		panic(err)
	}

	endpoint, err := host.Endpoint(ctx, "project")
	if err != nil {
		panic(err)
	}
	request := json.RawMessage(
		`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`,
	)
	if err := endpoint.Send(ctx, request); err != nil {
		panic(err)
	}
	fmt.Println(string(<-endpoint.Messages()))
}

Send accepts one complete JSON value and Messages emits complete ACP JSON responses and notifications in server order. One consumer should continuously drain each endpoint. A slow consumer applies backpressure; output is never silently discarded.

Use ReplaceWorkspace when configuration changes and RemoveWorkspace when a workspace is no longer available. Both operations cancel active turns and close the old sessions before returning. Calling Endpoint after an endpoint closes starts a fresh instance but never resends an uncertain request.

The host deliberately does not provide authentication, network transport, replay journals, or automatic restart. Those policies belong to the embedding application. If AllowedModels is non-empty, the runtime’s initial model must appear in it; model changes through ACP, slash commands, and subagents remain restricted to the same list.