@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.
- package/dist/agents-host.d.ts +2 -0
- package/dist/agents-host.js +111 -44
- package/dist/chat/sdk-chat-control-plane.js +3 -0
- package/dist/context/injection.d.ts +5 -3
- package/dist/context/injection.js +45 -29
- package/dist/context/prompt.js +23 -35
- package/dist/context/skill-manual.d.ts +13 -0
- package/dist/context/skill-manual.js +18 -0
- package/dist/context/turn-preparation.js +13 -4
- package/dist/gateway/localhost-gateway.js +16 -8
- package/dist/hosted-turn-content.d.ts +15 -0
- package/dist/hosted-turn-content.js +50 -0
- package/dist/managed-daemon.d.ts +3 -2
- package/dist/managed-daemon.js +77 -29
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +10 -0
- package/dist/providers/claude/cli-client.d.ts +11 -2
- package/dist/providers/claude/cli-client.js +98 -27
- package/dist/providers/codex/adapter.d.ts +3 -1
- package/dist/providers/codex/adapter.js +10 -0
- package/dist/providers/codex/cli-client.d.ts +10 -2
- package/dist/providers/codex/cli-client.js +85 -21
- package/dist/providers/codex/project-doc.js +13 -29
- package/dist/providers/copilot/adapter.d.ts +3 -1
- package/dist/providers/copilot/adapter.js +10 -0
- package/dist/providers/copilot/cli-client.d.ts +9 -1
- package/dist/providers/copilot/cli-client.js +71 -8
- package/dist/providers/create-provider.d.ts +1 -1
- package/dist/providers/create-provider.js +16 -5
- package/dist/providers/provider-adapter.d.ts +35 -0
- package/dist/providers/provider-adapter.js +44 -1
- package/dist/state-paths.d.ts +9 -1
- package/dist/state-paths.js +22 -3
- package/dist/types.d.ts +33 -2
- package/package.json +1 -1
- package/skills/borgee-agent/SKILL.md +119 -38
- package/skills/borgee-agent/references/errors.md +38 -0
- package/skills/borgee-agent/references/task-properties.md +30 -0
- package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
- package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
- package/skills/borgee-agent/borgee-agent.mjs +0 -562
- package/skills/borgee-agent/borgee-agent.py +0 -469
package/dist/context/prompt.js
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
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.
|
|
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(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
55
|
-
(context.
|
|
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('
|
|
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
|
-
...
|
|
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
|
-
|
|
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 &&
|
|
101
|
-
? { 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
|
|
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
|
|
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
|
|
903
|
-
// Falling through to 502 would tell the agent the server
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
//
|
|
912
|
-
//
|
|
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
|
+
}
|
package/dist/managed-daemon.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
208
|
+
private ownsEndpoint;
|
|
208
209
|
private shutdownRequested;
|
|
209
210
|
private ownedSocketInode;
|
|
210
211
|
constructor(rootPath: string, debug?: boolean, deps?: ManagedAgentsHostDaemonDeps);
|
package/dist/managed-daemon.js
CHANGED
|
@@ -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,
|
|
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(
|
|
603
|
-
|
|
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: ${
|
|
616
|
+
throw new Error(`Managed control socket path has an unexpected value: ${endpoint.address}`);
|
|
614
617
|
}
|
|
615
|
-
if (await canConnectToSocket(
|
|
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: ${
|
|
622
|
+
throw new Error(`Managed control socket path has an unexpected value: ${endpoint.address}`);
|
|
620
623
|
}
|
|
621
|
-
await fs.rm(
|
|
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
|
|
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(
|
|
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: ${
|
|
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
|
|
796
|
+
const endpoint = resolveManagedDaemonEndpoint(rootPath);
|
|
794
797
|
while (Date.now() < deadline) {
|
|
795
|
-
const listening = await canConnect(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1541
|
+
socketPath: this.endpoint.address,
|
|
1539
1542
|
});
|
|
1540
1543
|
}
|
|
1541
1544
|
catch (error) {
|
|
1542
1545
|
this.started = false;
|
|
1543
1546
|
this.ready = false;
|
|
1544
|
-
|
|
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
|
-
|
|
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.
|
|
1562
|
-
const socketStats = await fs.lstat(this.
|
|
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.
|
|
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.
|
|
1623
|
+
this.server.listen(this.endpoint.address);
|
|
1588
1624
|
});
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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)
|
|
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
|
|
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
|
|
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;
|