@borgee/agents-host 0.2.35 → 0.2.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/agents-host.d.ts +2 -0
  2. package/dist/agents-host.js +111 -44
  3. package/dist/chat/sdk-chat-control-plane.js +3 -0
  4. package/dist/context/injection.d.ts +5 -3
  5. package/dist/context/injection.js +45 -29
  6. package/dist/context/prompt.js +23 -35
  7. package/dist/context/skill-manual.d.ts +13 -0
  8. package/dist/context/skill-manual.js +18 -0
  9. package/dist/context/turn-preparation.js +13 -4
  10. package/dist/gateway/localhost-gateway.js +16 -8
  11. package/dist/hosted-turn-content.d.ts +15 -0
  12. package/dist/hosted-turn-content.js +50 -0
  13. package/dist/managed-daemon.d.ts +3 -2
  14. package/dist/managed-daemon.js +77 -29
  15. package/dist/providers/claude/adapter.d.ts +3 -1
  16. package/dist/providers/claude/adapter.js +10 -0
  17. package/dist/providers/claude/cli-client.d.ts +11 -2
  18. package/dist/providers/claude/cli-client.js +98 -27
  19. package/dist/providers/codex/adapter.d.ts +3 -1
  20. package/dist/providers/codex/adapter.js +10 -0
  21. package/dist/providers/codex/cli-client.d.ts +10 -2
  22. package/dist/providers/codex/cli-client.js +85 -21
  23. package/dist/providers/codex/project-doc.js +13 -29
  24. package/dist/providers/copilot/adapter.d.ts +3 -1
  25. package/dist/providers/copilot/adapter.js +10 -0
  26. package/dist/providers/copilot/cli-client.d.ts +9 -1
  27. package/dist/providers/copilot/cli-client.js +71 -8
  28. package/dist/providers/create-provider.d.ts +1 -1
  29. package/dist/providers/create-provider.js +16 -5
  30. package/dist/providers/provider-adapter.d.ts +35 -0
  31. package/dist/providers/provider-adapter.js +44 -1
  32. package/dist/state-paths.d.ts +9 -1
  33. package/dist/state-paths.js +22 -3
  34. package/dist/types.d.ts +33 -2
  35. package/package.json +1 -1
  36. package/skills/borgee-agent/SKILL.md +119 -38
  37. package/skills/borgee-agent/references/errors.md +38 -0
  38. package/skills/borgee-agent/references/task-properties.md +30 -0
  39. package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
  40. package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
  41. package/skills/borgee-agent/borgee-agent.mjs +0 -562
  42. package/skills/borgee-agent/borgee-agent.py +0 -469
@@ -3,6 +3,7 @@ import { buildAttentionSummaryLines } from './attention.js';
3
3
  import { buildCollaborationCapabilityDeclarationSummaryLines, buildMissedCollaborationDiagnosticSummaryLines, } from './collaboration-capabilities-diagnostics.js';
4
4
  import { buildCollaborationOutcomeSummaryLines } from './collaboration-outcome.js';
5
5
  import { MAIN_SESSION_DELEGATION_LINES } from './main-session-delegation.js';
6
+ import { buildSkillManualLines } from './skill-manual.js';
6
7
  import { buildTaskThreadCollaborationSummaryLines } from './task-thread-collaboration.js';
7
8
  function providerLabel(provider) {
8
9
  if (provider === 'copilot') {
@@ -13,50 +14,39 @@ function providerLabel(provider) {
13
14
  }
14
15
  return 'Claude';
15
16
  }
16
- function buildSkillRuntimePromptLines(context) {
17
- if (!context?.skillRuntime) {
17
+ // Claude and Codex name the manual once on their session-scoped surfaces. Copilot has no
18
+ // session-scoped surface — this prompt is its only model-facing text — so for Copilot alone the
19
+ // pointer rides every turn.
20
+ function buildSkillManualPromptLines(provider, context) {
21
+ if (provider !== 'copilot' || !context?.skillRuntime || !context.gatewayCredentialPath) {
18
22
  return [];
19
23
  }
20
- return [
21
- '',
22
- 'Read-only local skill runtime bootstrap is available for this turn.',
23
- `Channel context payload: ${context.channelContextPayloadPath}`,
24
- `Skill guide: ${context.skillRuntime.skillMarkdownPath}`,
25
- `Node CLI: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --print-bootstrap`,
26
- `Python CLI: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --print-bootstrap`,
27
- 'These CLIs are local-only, may only access the loopback gateway described below, must not mutate files or the local environment, and may only send auxiliary collaboration messages when that gateway surface is explicitly advertised below.',
28
- ];
24
+ return ['', ...buildSkillManualLines(context.skillRuntime)];
29
25
  }
26
+ // Only the facts that change from turn to turn: which credential file authorizes this turn, which
27
+ // task grammar the thread shape licenses, and whether the collaboration commands can be reached at
28
+ // all. Everything else a model needs to invoke the CLI is in the manual, which does not change.
30
29
  function buildLocalhostGatewayPromptLines(context) {
31
30
  if (!context?.localhostGateway) {
32
31
  return [];
33
32
  }
34
- if (!context.gatewayAuthPath) {
33
+ if (!context.gatewayCredentialPath) {
35
34
  return [];
36
35
  }
37
- const lines = [
38
- '',
39
- 'A loopback-only localhost gateway is available for this turn.',
40
- 'Use the packaged local CLI with the existing --context payload to access the documented gateway surface.',
41
- `Gateway auth sidecar for this turn: ${context.gatewayAuthPath}`,
42
- ];
43
- const isTaskAssignmentThread = context.taskAssignmentContext?.active === true;
36
+ const lines = ['', `Gateway credential file for this turn: ${context.gatewayCredentialPath}`];
44
37
  if (!context.skillRuntime) {
45
38
  return lines;
46
39
  }
47
- lines.push('The read-only gateway commands listed in this prompt are already authorized for this turn and may be executed directly.', 'Do not ask the user for permission before using the read-only gateway commands listed below.', 'If the user asks about channel history, visible participants, or your current agent identity, run the relevant read command first and answer from its result instead of speculating about authorization.', `Node gateway checks: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --health`, `Node channel bootstrap: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --read-bootstrap`, `Node agent identity: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --get-me`, `Node channel history: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --read-history --limit 20`, `Python gateway checks: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --health`, `Python channel bootstrap: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --read-bootstrap`, `Python agent identity: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --get-me`, `Python channel history: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --read-history --limit 20`);
48
- if (isTaskAssignmentThread) {
49
- lines.push('Task-collection commands remain disabled inside this task thread; use the parent channel for create/list task operations.', `Node get current thread task: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --get-task`, `Node update current thread task: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --update-task [--status "<status>"] [--assignee-id "<assignee-id>"] [--title "<title>"]`, `Python get current thread task: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --get-task`, `Python update current thread task: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --update-task [--status "<status>"] [--assignee-id "<assignee-id>"] [--title "<title>"]`, `Node set current thread task property: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --set-property "<key>=<value>"`, `Node delete current thread task property: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --delete-property "<key>"`, `Python set current thread task property: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --set-property "<key>=<value>"`, `Python delete current thread task property: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --delete-property "<key>"`, 'Task property keys are a closed set: link.pr (the PR implementing the task) and link.issue (the originating issue). agent.session_id is written by agents-host — do not set it. One key per call; pass <key>=<value> as one argument.');
50
- }
51
- else {
52
- lines.push(`Node create task: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --create-task --title "<title>" [--description "<description>"] [--assignee-id "<assignee-id>"]`, `Node list tasks: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --list-tasks`, `Node get task: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --get-task --task-id "<task-id>"`, `Node read task thread history: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --read-task-history --task-id "<task-id>" --limit 20`, `Node update task: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --update-task --task-id "<task-id>" [--status "<status>"] [--assignee-id "<assignee-id>"] [--title "<title>"]`, `Python create task: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --create-task --title "<title>" [--description "<description>"] [--assignee-id "<assignee-id>"]`, `Python list tasks: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --list-tasks`, `Python get task: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --get-task --task-id "<task-id>"`, `Python read task thread history: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --read-task-history --task-id "<task-id>" --limit 20`, `Python update task: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --update-task --task-id "<task-id>" [--status "<status>"] [--assignee-id "<assignee-id>"] [--title "<title>"]`, `Node set task property: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --set-property "<key>=<value>" --task-id "<task-id>"`, `Node delete task property: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --delete-property "<key>" --task-id "<task-id>"`, `Python set task property: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --set-property "<key>=<value>" --task-id "<task-id>"`, `Python delete task property: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --delete-property "<key>" --task-id "<task-id>"`, 'Task property keys are a closed set: link.pr (the PR implementing the task) and link.issue (the originating issue). agent.session_id is written by agents-host — do not set it. One key per call; pass <key>=<value> as one argument.');
40
+ lines.push(context.taskAssignmentContext?.active === true
41
+ ? 'This turn runs inside a task assignment thread.'
42
+ : 'This turn runs in a parent channel, not a task thread.');
43
+ const collaborationLive = context.localhostGateway.collaboration?.enabled === true
44
+ && (context.collaborationTurnMode ?? 'ordinary') === 'ordinary';
45
+ if (!collaborationLive) {
46
+ lines.push('Collaboration commands are not available this turn.');
53
47
  }
54
- if (context.localhostGateway.collaboration?.enabled &&
55
- (context.collaborationTurnMode ?? 'ordinary') === 'ordinary') {
56
- const turnExecutionArgument = context.collaborationTurnExecutionId
57
- ? ` --turn-execution-id ${context.collaborationTurnExecutionId}`
58
- : '';
59
- lines.push('Auxiliary collaboration commands are available for this turn. Use them only for short targeted escalation, mention, or reply notices, never for the main final answer body.', 'When you need the host-private in-flight draft for this live collaboration turn, use the dedicated read-draft command rather than inspecting public channel messages.', 'When --list-users is listed below, it is a read-only command for this turn and may be executed directly without asking for permission.', 'When the user explicitly asks you to mention, ping, notify, or send a short note to another visible participant, use the dedicated send-mention command directly and include a visible <@targetId> token in the body.', 'If the user asks you to @ another agent now, send the short auxiliary mention first, then confirm what you sent.', 'If the user says "the other agent" and only one other visible agent is present, resolve that target from --list-users and then use --send-mention with that participant id.', 'If collaboration with another visible agent would help, use these auxiliary commands to send a short targeted mention or reply-thread note yourself rather than asking AgentsHost to orchestrate a special protocol.', `Node private draft snapshot: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath}${turnExecutionArgument} --read-draft`, `Node user directory: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --list-users`, `Node auxiliary mention: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath}${turnExecutionArgument} --send-mention user-id --body "Need review from <@user-id>"`, `Node reply thread notice: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath}${turnExecutionArgument} --send-message --body "Following up here" --reply-to message-id`, `Python private draft snapshot: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath}${turnExecutionArgument} --read-draft`, `Python user directory: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --list-users`, `Python auxiliary mention: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath}${turnExecutionArgument} --send-mention user-id --body "Need review from <@user-id>"`, `Python reply thread notice: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath}${turnExecutionArgument} --send-message --body "Following up here" --reply-to message-id`);
48
+ else if (context.collaborationTurnExecutionId) {
49
+ lines.push(`Pass --turn-execution-id ${context.collaborationTurnExecutionId} on send, mention and draft.`);
60
50
  }
61
51
  return lines;
62
52
  }
@@ -77,7 +67,6 @@ function buildTaskAssignmentPromptLines(params) {
77
67
  ];
78
68
  if (taskAssignmentContext?.currentTaskId) {
79
69
  lines.push(`Current assigned task id: ${taskAssignmentContext.currentTaskId}.`);
80
- lines.push('Inside this task thread, the packaged Node and Python CLIs may omit --task-id for --get-task and --update-task because the current task id is already persisted in the injected thread context.');
81
70
  if (params.promptContext?.taskWorkspace) {
82
71
  lines.push(`Task-scoped writable workspace root: ${params.promptContext.taskWorkspace.rootPath}.`);
83
72
  lines.push('This writable workspace is local to agents-host for the current task thread. It does not imply that the target repository has already been checked out there.');
@@ -85,9 +74,8 @@ function buildTaskAssignmentPromptLines(params) {
85
74
  }
86
75
  else if (taskAssignmentContext?.active === true) {
87
76
  lines.push('No current task id is persisted in the injected thread context for this task thread.');
88
- lines.push('Inside this task thread, shorthand --get-task/--update-task calls without --task-id use an agents-host local fallback that scans visible parent-channel tasks for this thread; pass --task-id explicitly if that fallback cannot resolve a unique task.');
89
77
  }
90
- lines.push('Set the task status to in_progress when you start.', 'When you finish, set the task status to in_review and return your normal final response in this thread.', 'Do not use an auxiliary message command for the main completion report; reserve it for intentional targeted escalation or cross-channel notification.', 'Do not use create/list task operations inside this task thread; those remain parent-channel operations.', 'Do not move the main work back to the parent channel.');
78
+ lines.push('Return your normal final response in this thread, and do not move the main work back to the parent channel.');
91
79
  return lines;
92
80
  }
93
81
  function buildInboundMetadataLines(params) {
@@ -247,7 +235,7 @@ export function buildPrompt(params) {
247
235
  const lines = buildMissedCollaborationDiagnosticSummaryLines(params.promptContext?.missedCollaborationDiagnostic);
248
236
  return lines.length > 0 ? ['', ...lines] : [];
249
237
  })(),
250
- ...buildSkillRuntimePromptLines(params.promptContext),
238
+ ...buildSkillManualPromptLines(params.provider, params.promptContext),
251
239
  ...buildLocalhostGatewayPromptLines(params.promptContext),
252
240
  '',
253
241
  `New message from ${params.incomingAuthorId}:`,
@@ -0,0 +1,13 @@
1
+ import type { SkillRuntimeBootstrapMetadata } from '../types.js';
2
+ /**
3
+ * What every model-facing surface says about the packaged CLI: what it is, where its manual is, and
4
+ * that the manual has to be opened before acting. Nothing else about the CLI is stated on a host
5
+ * surface — the manual is self-contained, so the invocation grammar, the command inventory, the
6
+ * limits and the credential-file rules have one home that cannot drift from the implementations.
7
+ *
8
+ * The authorization sentence is written against the credential file rather than against "this turn"
9
+ * because the surfaces disagree about scope: the per-turn prompt is turn-scoped while Claude's
10
+ * session append and Codex's projected document are session-scoped, and only the per-turn prompt
11
+ * knows whether a credential file exists.
12
+ */
13
+ export declare function buildSkillManualLines(skillRuntime: SkillRuntimeBootstrapMetadata): string[];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What every model-facing surface says about the packaged CLI: what it is, where its manual is, and
3
+ * that the manual has to be opened before acting. Nothing else about the CLI is stated on a host
4
+ * surface — the manual is self-contained, so the invocation grammar, the command inventory, the
5
+ * limits and the credential-file rules have one home that cannot drift from the implementations.
6
+ *
7
+ * The authorization sentence is written against the credential file rather than against "this turn"
8
+ * because the surfaces disagree about scope: the per-turn prompt is turn-scoped while Claude's
9
+ * session append and Codex's projected document are session-scoped, and only the per-turn prompt
10
+ * knows whether a credential file exists.
11
+ */
12
+ export function buildSkillManualLines(skillRuntime) {
13
+ return [
14
+ "A packaged local CLI reads this Borgee channel and acts on its tasks: channel history, visible participants, this channel's tasks and their properties, and short auxiliary mentions.",
15
+ `Read its manual at ${skillRuntime.skillMarkdownPath} before you act on this channel; you cannot form a working invocation without it.`,
16
+ 'Every command the manual documents is already authorized on a turn whose prompt names a gateway credential file. Run them directly and do not ask the user for permission first.',
17
+ ];
18
+ }
@@ -1,4 +1,5 @@
1
1
  import { HostLogger, summarizeError } from '../debug.js';
2
+ import { buildHostedIncomingContentText, buildHostedTurnContentParts } from '../hosted-turn-content.js';
2
3
  import { ChannelContextPreparationError, } from './injection.js';
3
4
  import { buildPrompt } from './prompt.js';
4
5
  function providerLabel(provider) {
@@ -24,7 +25,7 @@ function toPromptContext(channelContext, input) {
24
25
  ...(channelContext
25
26
  ? {
26
27
  channelContextPayloadPath: channelContext.payloadPath,
27
- gatewayAuthPath: channelContext.gatewayAuthPath,
28
+ gatewayCredentialPath: channelContext.gatewayCredentialPath,
28
29
  skillRuntime: channelContext.skillRuntime,
29
30
  localhostGateway: channelContext.localhostGateway,
30
31
  taskAssignmentContext: channelContext.payload.taskAssignmentContext,
@@ -75,6 +76,12 @@ export class ProviderTurnPreparer {
75
76
  this.logger = logger;
76
77
  }
77
78
  async prepare(input) {
79
+ const incomingParts = input.incomingParts ?? buildHostedTurnContentParts({
80
+ text: input.incomingContent,
81
+ });
82
+ const incomingContent = incomingParts.length > 0
83
+ ? buildHostedIncomingContentText(incomingParts)
84
+ : input.incomingContent;
78
85
  let channelContext;
79
86
  if (this.channelContextStore) {
80
87
  try {
@@ -97,8 +104,8 @@ export class ProviderTurnPreparer {
97
104
  ...(input.incomingMessageType !== undefined
98
105
  ? { incomingMessageType: input.incomingMessageType }
99
106
  : {}),
100
- ...(input.incomingMessageType !== undefined && input.incomingContent !== undefined
101
- ? { incomingContent: input.incomingContent }
107
+ ...(input.incomingMessageType !== undefined && incomingContent !== undefined
108
+ ? { incomingContent }
102
109
  : {}),
103
110
  };
104
111
  channelContext = await this.channelContextStore.prepare(prepareInput);
@@ -116,6 +123,8 @@ export class ProviderTurnPreparer {
116
123
  const promptContext = toPromptContext(channelContext, input);
117
124
  return {
118
125
  channelId: input.channelId,
126
+ incomingContent,
127
+ incomingParts,
119
128
  incomingEventKind: input.incomingEventKind,
120
129
  incomingMessageType: input.incomingMessageType,
121
130
  prompt: buildPrompt({
@@ -123,7 +132,7 @@ export class ProviderTurnPreparer {
123
132
  provider: input.provider,
124
133
  channelId: input.channelId,
125
134
  incomingAuthorId: input.incomingAuthorId,
126
- incomingContent: input.incomingContent,
135
+ incomingContent,
127
136
  incomingEventKind: input.incomingEventKind,
128
137
  incomingMessageType: input.incomingMessageType,
129
138
  promptContext,
@@ -655,8 +655,14 @@ class LoopbackLocalhostGatewayController {
655
655
  this.recordAudit('authorized', 200, decision.path, method ?? 'PUT', decision.binding);
656
656
  }
657
657
  async loadCurrentThreadTask(binding) {
658
+ // Only a task-thread binding has a current task at all. Without that context the scan below
659
+ // would list every visible channel's tasks to conclude what the binding already says, so the
660
+ // parent-channel answer is given here instead of after an O(channels x tasks) sweep.
661
+ if (binding.payload?.taskAssignmentContext?.active !== true) {
662
+ throw new GatewayHttpError(404, { error: 'not_found' }, 'not-found');
663
+ }
658
664
  let task;
659
- const preferredTaskId = binding.payload?.taskAssignmentContext?.currentTaskId;
665
+ const preferredTaskId = binding.payload.taskAssignmentContext.currentTaskId;
660
666
  try {
661
667
  task = await findTaskForThread(this.controlPlane, binding.channelId, preferredTaskId);
662
668
  }
@@ -899,17 +905,19 @@ function mapGatewayControlPlaneError(error, fallbackReason) {
899
905
  case 'bpp.channel_id_required':
900
906
  case 'bpp.task_id_required':
901
907
  return new GatewayHttpError(400, { error: 'bad_request' }, 'bad-request');
902
- // An unregistered key or an oversized value is the caller's own mistake.
903
- // Falling through to 502 would tell the agent the server is broken, and it
904
- // would retry the same bad call instead of correcting it.
908
+ // An unregistered key, an oversized value or a status outside the enum is the
909
+ // caller's own mistake. Falling through to 502 would tell the agent the server
910
+ // is broken, and it would retry the same bad call instead of correcting it.
905
911
  case 'bpp.task_property_key_unknown':
906
912
  return new GatewayHttpError(400, { error: 'unknown_property_key' }, 'bad-request');
907
913
  case 'bpp.task_property_value_too_long':
908
914
  return new GatewayHttpError(400, { error: 'property_value_too_long' }, 'bad-request');
909
- // A distinct body, not the bare `not_found` the other routes use: the CLI
910
- // reads that one on a current-task request as "the thread's task could not
911
- // be resolved" and tells the agent to pass --task-id, which is the wrong
912
- // correction for a key that simply was not set.
915
+ case 'bpp.task_invalid_status':
916
+ return new GatewayHttpError(400, { error: 'invalid_status' }, 'bad-request');
917
+ // A distinct body, not the bare `not_found` the other routes use: on a
918
+ // current-task property request `not_found` means the thread's own task
919
+ // could not be resolved, and "pass the task id" is the wrong correction
920
+ // for a key that simply was not set.
913
921
  case 'bpp.task_property_not_found':
914
922
  return new GatewayHttpError(404, { error: 'property_not_found' }, 'not-found');
915
923
  default:
@@ -0,0 +1,15 @@
1
+ import type { HostedMessageAttachment, HostedTurnContentPart } from './types.js';
2
+ type CanonicalHostedMessageAttachment = HostedMessageAttachment & {
3
+ kind: NonNullable<HostedMessageAttachment['kind']>;
4
+ };
5
+ export declare function normalizeHostedMessageAttachment(attachment: HostedMessageAttachment): CanonicalHostedMessageAttachment;
6
+ export declare function classifyHostedAttachmentKind(contentType: string | undefined): Extract<HostedTurnContentPart, {
7
+ type: 'image' | 'file';
8
+ }>['type'];
9
+ export declare function buildHostedTurnContentParts(input: {
10
+ text?: string;
11
+ attachments?: readonly HostedMessageAttachment[];
12
+ }): HostedTurnContentPart[];
13
+ export declare function extractHostedMessageAttachments(parts: readonly HostedTurnContentPart[]): HostedMessageAttachment[];
14
+ export declare function buildHostedIncomingContentText(parts: readonly HostedTurnContentPart[]): string;
15
+ export {};
@@ -0,0 +1,50 @@
1
+ export function normalizeHostedMessageAttachment(attachment) {
2
+ return {
3
+ url: attachment.url,
4
+ ...(attachment.filename ? { filename: attachment.filename } : {}),
5
+ contentType: attachment.contentType,
6
+ kind: attachment.kind ?? classifyHostedAttachmentKind(attachment.contentType),
7
+ ...(attachment.sizeBytes !== undefined ? { sizeBytes: attachment.sizeBytes } : {}),
8
+ };
9
+ }
10
+ export function classifyHostedAttachmentKind(contentType) {
11
+ return contentType?.toLowerCase().startsWith('image/') ? 'image' : 'file';
12
+ }
13
+ export function buildHostedTurnContentParts(input) {
14
+ const text = input.text?.trim() ?? '';
15
+ const attachments = input.attachments ?? [];
16
+ const parts = [];
17
+ if (text.length > 0) {
18
+ parts.push({ type: 'text', text });
19
+ }
20
+ for (const attachment of attachments) {
21
+ const normalizedAttachment = normalizeHostedMessageAttachment(attachment);
22
+ parts.push({
23
+ type: normalizedAttachment.kind,
24
+ attachment: normalizedAttachment,
25
+ });
26
+ }
27
+ return parts;
28
+ }
29
+ export function extractHostedMessageAttachments(parts) {
30
+ return parts.flatMap((part) => ('attachment' in part ? [normalizeHostedMessageAttachment(part.attachment)] : []));
31
+ }
32
+ export function buildHostedIncomingContentText(parts) {
33
+ const text = parts
34
+ .flatMap((part) => (part.type === 'text' ? [part.text] : []))
35
+ .join('\n')
36
+ .trim();
37
+ const nonTextParts = parts.filter((part) => part.type === 'image' || part.type === 'file');
38
+ if (nonTextParts.length === 0) {
39
+ return text;
40
+ }
41
+ const summaryLines = [
42
+ `[Attachment metadata was received for ${nonTextParts.length} attachment${nonTextParts.length === 1 ? '' : 's'}]`,
43
+ ...nonTextParts.map((part, index) => {
44
+ const filename = part.attachment.filename?.trim();
45
+ return `${index + 1}. ${part.type};${filename ? ` filename: ${filename};` : ''} content type: ${part.attachment.contentType}`;
46
+ }),
47
+ ];
48
+ const summary = summaryLines.join('\n');
49
+ return text ? `${text}\n\n${summary}` : summary;
50
+ }
@@ -95,6 +95,7 @@ export interface ManagedAgentsHostDaemonDeps {
95
95
  materialize?: (rootPath: string, spec: LocalConfigGenerateSpec) => Promise<unknown>;
96
96
  logger?: Pick<Console, 'log' | 'error'>;
97
97
  logPath?: string;
98
+ platform?: NodeJS.Platform;
98
99
  }
99
100
  export interface DescribeManagedSpecOptions {
100
101
  serverUrl: string;
@@ -192,7 +193,7 @@ export declare function applyManagedSpec(options: ApplyManagedSpecOptions, deps?
192
193
  export declare class ManagedAgentsHostDaemon {
193
194
  private readonly rootPath;
194
195
  private readonly hostConfigPath;
195
- private readonly socketPath;
196
+ private readonly endpoint;
196
197
  private readonly createSupervisor;
197
198
  private readonly supervisor;
198
199
  private readonly loadSpec;
@@ -204,7 +205,7 @@ export declare class ManagedAgentsHostDaemon {
204
205
  private requestQueue;
205
206
  private started;
206
207
  private ready;
207
- private ownsSocket;
208
+ private ownsEndpoint;
208
209
  private shutdownRequested;
209
210
  private ownedSocketInode;
210
211
  constructor(rootPath: string, debug?: boolean, deps?: ManagedAgentsHostDaemonDeps);
@@ -8,7 +8,7 @@ import { AgentsHostSupervisor } from './agents-host-supervisor.js';
8
8
  import { CLAUDE_PROVIDER_V2_COMPATIBILITY_GATE, COMPATIBILITY_GATES_ENV, createManagedRuntimeSettingsFingerprint, INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV, INTERNAL_POLICY_MODE_ENV, INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV, MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE, MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION, resolveDisabledDefaultCompatibilityGates, resolveManagedRuntimeSettingsSnapshot, normalizeInternalProviderImplementationOverrides, } from './compatibility-gates.js';
9
9
  import { DEFAULT_PROVIDER_COMMAND_CONFIG, hasLegacyClaudeOneShotArgs, loadConfigFromEnv, } from './config.js';
10
10
  import { loadLocalConfigGenerateSpec, materializeLocalConfig, parseGenerateConfigSpec, resolveLocalConfigLayout, } from './local-config.js';
11
- import { normalizeManagedRuntimeKey, resolveManagedBootstrapLockPath, resolveManagedDaemonLogPath, resolveManagedDaemonSocketPath, resolveManagedRuntimeRoot, resolveManagedRuntimeSettingsPath, } from './state-paths.js';
11
+ import { normalizeManagedRuntimeKey, resolveManagedBootstrapLockPath, resolveManagedDaemonEndpoint, resolveManagedDaemonLogPath, resolveManagedRuntimeRoot, resolveManagedRuntimeSettingsPath, } from './state-paths.js';
12
12
  const MANAGED_ROOT_MODE = 0o700;
13
13
  const MANAGED_CONFIG_MODE = 0o600;
14
14
  const BOOTSTRAP_LOCK_WAIT_MS = 10_000;
@@ -599,8 +599,11 @@ async function canConnectToSocket(socketPath) {
599
599
  socket.once('error', () => finalize(false));
600
600
  });
601
601
  }
602
- async function removeStaleControlSocket(socketPath) {
603
- const stats = await fs.lstat(socketPath).catch((error) => {
602
+ async function removeStaleControlSocket(endpoint) {
603
+ if (endpoint.kind === 'named-pipe') {
604
+ return false;
605
+ }
606
+ const stats = await fs.lstat(endpoint.address).catch((error) => {
604
607
  if (isNotFoundError(error)) {
605
608
  return undefined;
606
609
  }
@@ -610,15 +613,15 @@ async function removeStaleControlSocket(socketPath) {
610
613
  return false;
611
614
  }
612
615
  if (stats.isDirectory() || stats.isSymbolicLink()) {
613
- throw new Error(`Managed control socket path has an unexpected value: ${socketPath}`);
616
+ throw new Error(`Managed control socket path has an unexpected value: ${endpoint.address}`);
614
617
  }
615
- if (await canConnectToSocket(socketPath)) {
618
+ if (await canConnectToSocket(endpoint.address)) {
616
619
  return false;
617
620
  }
618
621
  if (!stats.isSocket() && !stats.isFile()) {
619
- throw new Error(`Managed control socket path has an unexpected value: ${socketPath}`);
622
+ throw new Error(`Managed control socket path has an unexpected value: ${endpoint.address}`);
620
623
  }
621
- await fs.rm(socketPath, { force: true });
624
+ await fs.rm(endpoint.address, { force: true });
622
625
  return true;
623
626
  }
624
627
  function parseDaemonResponse(raw) {
@@ -655,12 +658,12 @@ function isManagedDaemonConnectionError(error) {
655
658
  isNodeErrorWithCode(error, 'ECONNRESET'));
656
659
  }
657
660
  export async function sendManagedDaemonRequest(rootPath, request) {
658
- const socketPath = resolveManagedDaemonSocketPath(rootPath);
661
+ const endpoint = resolveManagedDaemonEndpoint(rootPath);
659
662
  const requestTimeoutMs = request.type === 'upsertAgent' || request.type === 'applySpec'
660
663
  ? DAEMON_RECONCILE_WAIT_MS
661
664
  : DAEMON_READY_WAIT_MS;
662
665
  return new Promise((resolveRequest, rejectRequest) => {
663
- const socket = createConnection(socketPath);
666
+ const socket = createConnection(endpoint.address);
664
667
  let settled = false;
665
668
  let responseBuffer = '';
666
669
  const rejectOnce = (error) => {
@@ -681,7 +684,7 @@ export async function sendManagedDaemonRequest(rootPath, request) {
681
684
  };
682
685
  socket.setEncoding('utf8');
683
686
  socket.setTimeout(requestTimeoutMs);
684
- socket.once('timeout', () => rejectOnce(new Error(`Timed out talking to managed daemon: ${socketPath}`)));
687
+ socket.once('timeout', () => rejectOnce(new Error(`Timed out talking to managed daemon: ${endpoint.address}`)));
685
688
  socket.once('error', rejectOnce);
686
689
  socket.on('data', (chunk) => {
687
690
  responseBuffer += chunk;
@@ -790,9 +793,9 @@ async function readReadyManagedDaemonStatus(rootPath, sendRequest) {
790
793
  }
791
794
  async function waitForManagedDaemonShutdown(rootPath, canConnect) {
792
795
  const deadline = Date.now() + DAEMON_READY_WAIT_MS;
793
- const socketPath = resolveManagedDaemonSocketPath(rootPath);
796
+ const endpoint = resolveManagedDaemonEndpoint(rootPath);
794
797
  while (Date.now() < deadline) {
795
- const listening = await canConnect(socketPath).catch((error) => {
798
+ const listening = await canConnect(endpoint.address).catch((error) => {
796
799
  if (isNodeErrorWithCode(error, 'ENOENT')) {
797
800
  return false;
798
801
  }
@@ -1481,7 +1484,7 @@ export async function applyManagedSpec(options, deps = {}) {
1481
1484
  export class ManagedAgentsHostDaemon {
1482
1485
  rootPath;
1483
1486
  hostConfigPath;
1484
- socketPath;
1487
+ endpoint;
1485
1488
  createSupervisor;
1486
1489
  supervisor;
1487
1490
  loadSpec;
@@ -1493,14 +1496,14 @@ export class ManagedAgentsHostDaemon {
1493
1496
  requestQueue = Promise.resolve();
1494
1497
  started = false;
1495
1498
  ready = false;
1496
- ownsSocket = false;
1499
+ ownsEndpoint = false;
1497
1500
  shutdownRequested = false;
1498
1501
  ownedSocketInode = null;
1499
1502
  constructor(rootPath, debug = false, deps = {}) {
1500
1503
  const layout = resolveLocalConfigLayout(rootPath);
1501
1504
  this.rootPath = layout.root;
1502
1505
  this.hostConfigPath = layout.hostConfigPath;
1503
- this.socketPath = resolveManagedDaemonSocketPath(this.rootPath);
1506
+ this.endpoint = resolveManagedDaemonEndpoint(this.rootPath, deps.platform);
1504
1507
  this.createSupervisor =
1505
1508
  deps.createSupervisor ??
1506
1509
  ((configPath, daemonDebug) => new AgentsHostSupervisor(configPath, { debug: daemonDebug }));
@@ -1535,22 +1538,34 @@ export class ManagedAgentsHostDaemon {
1535
1538
  this.logger.log('[agents-host] managed daemon ready', {
1536
1539
  rootPath: this.rootPath,
1537
1540
  hostConfigPath: this.hostConfigPath,
1538
- socketPath: this.socketPath,
1541
+ socketPath: this.endpoint.address,
1539
1542
  });
1540
1543
  }
1541
1544
  catch (error) {
1542
1545
  this.started = false;
1543
1546
  this.ready = false;
1544
- await this.stop().catch(() => undefined);
1547
+ try {
1548
+ await this.stop();
1549
+ }
1550
+ catch (cleanupError) {
1551
+ this.logger.error('[agents-host] managed daemon startup cleanup failed', {
1552
+ rootPath: this.rootPath,
1553
+ error: cleanupError,
1554
+ });
1555
+ }
1545
1556
  throw error;
1546
1557
  }
1547
1558
  }
1548
1559
  async stop() {
1549
1560
  this.ready = false;
1561
+ let stopError;
1550
1562
  try {
1551
1563
  await this.supervisor.stop();
1552
1564
  }
1553
- finally {
1565
+ catch (error) {
1566
+ stopError = error;
1567
+ }
1568
+ try {
1554
1569
  await new Promise((resolveStop) => {
1555
1570
  if (!this.server.listening) {
1556
1571
  resolveStop();
@@ -1558,17 +1573,38 @@ export class ManagedAgentsHostDaemon {
1558
1573
  }
1559
1574
  this.server.close(() => resolveStop());
1560
1575
  });
1561
- if (this.ownsSocket) {
1562
- const socketStats = await fs.lstat(this.socketPath).catch(() => undefined);
1576
+ if (this.ownsEndpoint && this.endpoint.kind === 'unix-socket') {
1577
+ const socketStats = await fs.lstat(this.endpoint.address).catch((error) => {
1578
+ if (isNotFoundError(error)) {
1579
+ return undefined;
1580
+ }
1581
+ throw error;
1582
+ });
1563
1583
  if (socketStats && socketStats.ino === this.ownedSocketInode) {
1564
- await fs.rm(this.socketPath, { force: true }).catch(() => undefined);
1584
+ await fs.rm(this.endpoint.address, { force: true });
1565
1585
  }
1566
- this.ownsSocket = false;
1567
- this.ownedSocketInode = null;
1568
1586
  }
1587
+ }
1588
+ catch (cleanupError) {
1589
+ if (stopError) {
1590
+ this.logger.error('[agents-host] managed daemon control endpoint cleanup failed', {
1591
+ rootPath: this.rootPath,
1592
+ error: cleanupError,
1593
+ });
1594
+ }
1595
+ else {
1596
+ stopError = cleanupError;
1597
+ }
1598
+ }
1599
+ finally {
1600
+ this.ownsEndpoint = false;
1601
+ this.ownedSocketInode = null;
1569
1602
  this.started = false;
1570
1603
  this.shutdownRequested = false;
1571
1604
  }
1605
+ if (stopError) {
1606
+ throw stopError;
1607
+ }
1572
1608
  }
1573
1609
  async listen() {
1574
1610
  for (let attempt = 0; attempt < 2; attempt++) {
@@ -1584,20 +1620,32 @@ export class ManagedAgentsHostDaemon {
1584
1620
  };
1585
1621
  this.server.once('error', onError);
1586
1622
  this.server.once('listening', onListening);
1587
- this.server.listen(this.socketPath);
1623
+ this.server.listen(this.endpoint.address);
1588
1624
  });
1589
- await fs.chmod(this.socketPath, MANAGED_CONFIG_MODE);
1590
- this.ownedSocketInode = (await fs.lstat(this.socketPath)).ino;
1591
- this.ownsSocket = true;
1625
+ if (this.endpoint.kind === 'unix-socket') {
1626
+ await fs.chmod(this.endpoint.address, MANAGED_CONFIG_MODE);
1627
+ this.ownedSocketInode = (await fs.lstat(this.endpoint.address)).ino;
1628
+ }
1629
+ this.ownsEndpoint = true;
1592
1630
  return;
1593
1631
  }
1594
1632
  catch (error) {
1595
- if (!isSocketBusyError(error) || !(await removeStaleControlSocket(this.socketPath))) {
1633
+ if (!isSocketBusyError(error)) {
1634
+ throw error;
1635
+ }
1636
+ if (this.endpoint.kind === 'named-pipe') {
1637
+ if (await canConnectToSocket(this.endpoint.address)) {
1638
+ throw error;
1639
+ }
1640
+ await delay(DAEMON_READY_RETRY_MS);
1641
+ continue;
1642
+ }
1643
+ if (!(await removeStaleControlSocket(this.endpoint))) {
1596
1644
  throw error;
1597
1645
  }
1598
1646
  }
1599
1647
  }
1600
- throw new Error(`Unable to bind managed daemon control socket: ${this.socketPath}`);
1648
+ throw new Error(`Unable to bind managed daemon control endpoint: ${this.endpoint.address}`);
1601
1649
  }
1602
1650
  async handleSocket(socket) {
1603
1651
  let buffer = '';
@@ -1,10 +1,12 @@
1
- import type { ProviderAdapter } from '../provider-adapter.js';
1
+ import { type ProviderAdapter } from '../provider-adapter.js';
2
2
  import type { ProviderGenerateOptions, ProviderInput, ProviderReply } from '../../types.js';
3
3
  import { ProviderTurnPreparer } from '../../context/turn-preparation.js';
4
4
  import { ClaudeCliClient } from './cli-client.js';
5
+ export declare const CLAUDE_HOSTED_PROVIDER_CAPABILITIES: import("../provider-adapter.js").ProviderCapabilities;
5
6
  export declare class ClaudeProviderAdapter implements ProviderAdapter {
6
7
  private readonly cli;
7
8
  private readonly turnPreparer;
9
+ readonly capabilities: import("../provider-adapter.js").ProviderCapabilities;
8
10
  constructor(cli: ClaudeCliClient, turnPreparer: ProviderTurnPreparer);
9
11
  generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
10
12
  dispose(): Promise<void>;
@@ -1,7 +1,17 @@
1
+ import { createHostedProviderCapabilities } from '../provider-adapter.js';
1
2
  import { createAwaitingUserProgressHandler, parseProviderReply } from '../awaiting-user.js';
3
+ export const CLAUDE_HOSTED_PROVIDER_CAPABILITIES = createHostedProviderCapabilities({
4
+ imageInputTransport: {
5
+ source: 'transport-derived',
6
+ support: 'supported',
7
+ reason: 'real-media-delivered',
8
+ delivery: ['blob'],
9
+ },
10
+ });
2
11
  export class ClaudeProviderAdapter {
3
12
  cli;
4
13
  turnPreparer;
14
+ capabilities = CLAUDE_HOSTED_PROVIDER_CAPABILITIES;
5
15
  constructor(cli, turnPreparer) {
6
16
  this.cli = cli;
7
17
  this.turnPreparer = turnPreparer;