Phase 3 — typescript-client · typescript-client · size M · Parent epic #3713
Depends on: [agent-server] /api/agent-profiles router + provenance (#3719, #3720)
ts-client is a small, hand-written repo (no codegen). Work is purely additive. Split into 2 PRs only if the diff gets unwieldy.
Context
The client is a thin, hand-written wrapper (no codegen). Hard ordering: author only after the SDK schema + routers are frozen. No MCPProfilesClient is needed — mcp_server_refs is a plain string[] | null field. The ACP provider credential metadata (api_key_env_var, base_url_env_var, file_secrets) is static and goes directly into src/utils/acp.ts — no API call needed.
Scope
1. AgentProfile union + client
AgentProfile discriminated union (on agent_kind) in src/models/agent-profile.ts:
- OpenHands variant:
{ id, name, revision, llm_profile_ref: string, mcp_server_refs: string[] | null, agent, skills, system_message_suffix, condenser, verification, enable_sub_agents, tool_concurrency_limit }
- ACP variant:
{ id, name, revision, acp_server, acp_model, acp_session_mode, acp_prompt_timeout, acp_command, acp_args, mcp_server_refs: string[] | null } — no llm_profile_ref, no credential field
AgentProfileSummary for lists.
src/client/agent-profiles-client.ts over /api/agent-profiles (list/get/save/delete/rename/activate/materialize), surfacing 409-on-referenced + 422-dangling-mcp-server-ref errors via HttpError status.
- Typed
agent_profile_id on the conversation-create path (CreateConversationPayload is Record<string, unknown> today — additive, no break).
2. ACP provider descriptor extension
3. Resolve/materialize types + deriveSwitchPlan
- Type the materialize response (resolved
AgentSettingsConfig union + ref resolved/dangling status + valid verdict) and the LaunchedProfile { id, revision, snapshot } provenance.
- Pure
deriveSwitchPlan(snapshot, targetProfile, providerInfo) → current | switch-live(mutableFields) | start-new(reason) | disabled(reason), consuming ACPProviderInfo.supports_runtime_model_switch + the existing switchProfile/switchLLM/switchAcpModel methods (conversation-client.ts:262–283). OpenHands→OpenHands live only if just the LLM differs; ACP→ACP live only if same provider + identity and only acp_model differs and the provider supports it; kind change never live.
Wiring/publish: dedicated clients only. Wire into ConversationManager, export from src/clients.ts + src/index.ts, add api-clients.test.ts coverage. Publishing is tag-driven; canvas re-pins via the usual temp-SHA dance.
Acceptance criteria
- Exported types match the SDK schemas field-for-field; union narrows on
agent_kind (both variants have mcp_server_refs: string[] | null).
ACPProviderInfo has credential metadata fields for all three providers; no hardcoded provider lists in canvas.
- FK error statuses surfaced;
deriveSwitchPlan is pure + unit-tested across all ACP providers + OpenHands.
tsc clean; new clients tested.
Phase 3 — typescript-client ·
typescript-client· size M · Parent epic #3713Depends on: [agent-server]
/api/agent-profilesrouter + provenance (#3719, #3720)Context
The client is a thin, hand-written wrapper (no codegen). Hard ordering: author only after the SDK schema + routers are frozen. No
MCPProfilesClientis needed —mcp_server_refsis a plainstring[] | nullfield. The ACP provider credential metadata (api_key_env_var,base_url_env_var,file_secrets) is static and goes directly intosrc/utils/acp.ts— no API call needed.Scope
1. AgentProfile union + client
AgentProfilediscriminated union (onagent_kind) insrc/models/agent-profile.ts:{ id, name, revision, llm_profile_ref: string, mcp_server_refs: string[] | null, agent, skills, system_message_suffix, condenser, verification, enable_sub_agents, tool_concurrency_limit }{ id, name, revision, acp_server, acp_model, acp_session_mode, acp_prompt_timeout, acp_command, acp_args, mcp_server_refs: string[] | null }— nollm_profile_ref, no credential fieldAgentProfileSummaryfor lists.src/client/agent-profiles-client.tsover/api/agent-profiles(list/get/save/delete/rename/activate/materialize), surfacing409-on-referenced +422-dangling-mcp-server-ref errors viaHttpErrorstatus.agent_profile_idon the conversation-create path (CreateConversationPayloadisRecord<string, unknown>today — additive, no break).2. ACP provider descriptor extension
ACPProviderInfoinsrc/utils/acp.tswith:api_key_env_var?: string,base_url_env_var?: string,file_secrets?: { secret_name: string; filename: string; env_var: string }[]. Populate for each existing provider (claude-code, codex, gemini). This is static metadata — no API call needed, no server endpoint. The canvas ACP authentication section ([AgentProfile][canvas] Provider-driven ACP Authentication section (creds out of the generic Secrets panel) #3728) reads these fields to render the credential form.3. Resolve/materialize types +
deriveSwitchPlanAgentSettingsConfigunion + ref resolved/dangling status + valid verdict) and theLaunchedProfile { id, revision, snapshot }provenance.deriveSwitchPlan(snapshot, targetProfile, providerInfo) → current | switch-live(mutableFields) | start-new(reason) | disabled(reason), consumingACPProviderInfo.supports_runtime_model_switch+ the existingswitchProfile/switchLLM/switchAcpModelmethods (conversation-client.ts:262–283). OpenHands→OpenHands live only if just the LLM differs; ACP→ACP live only if same provider + identity and onlyacp_modeldiffers and the provider supports it; kind change never live.Wiring/publish: dedicated clients only. Wire into
ConversationManager, export fromsrc/clients.ts+src/index.ts, addapi-clients.test.tscoverage. Publishing is tag-driven; canvas re-pins via the usual temp-SHA dance.Acceptance criteria
agent_kind(both variants havemcp_server_refs: string[] | null).ACPProviderInfohas credential metadata fields for all three providers; no hardcoded provider lists in canvas.deriveSwitchPlanis pure + unit-tested across all ACP providers + OpenHands.tscclean; new clients tested.