@borgee/agents-host 0.2.33 → 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/README.md +28 -7
- package/dist/agents-host.d.ts +19 -0
- package/dist/agents-host.js +163 -44
- package/dist/chat/chat-control-plane.d.ts +9 -0
- package/dist/chat/sdk-chat-control-plane.d.ts +10 -1
- package/dist/chat/sdk-chat-control-plane.js +9 -0
- package/dist/cli-args.d.ts +1 -1
- package/dist/cli-args.js +5 -0
- package/dist/compatibility-gates.d.ts +1 -0
- package/dist/compatibility-gates.js +2 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +21 -3
- package/dist/context/injection.d.ts +5 -3
- package/dist/context/injection.js +45 -29
- package/dist/context/main-session-delegation.d.ts +1 -0
- package/dist/context/main-session-delegation.js +6 -0
- package/dist/context/prompt.js +25 -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 +75 -1
- package/dist/hosted-turn-content.d.ts +15 -0
- package/dist/hosted-turn-content.js +50 -0
- package/dist/local-config.js +10 -1
- package/dist/managed-daemon.d.ts +3 -2
- package/dist/managed-daemon.js +82 -29
- package/dist/plugin-sdk.js +58 -1
- package/dist/plugin-sdk.js.map +2 -2
- package/dist/policy/gateway-authorization.d.ts +18 -3
- package/dist/policy/gateway-authorization.js +33 -1
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +13 -1
- package/dist/providers/claude/cli-client.d.ts +36 -3
- package/dist/providers/claude/cli-client.js +225 -37
- package/dist/providers/codex/adapter.d.ts +3 -1
- package/dist/providers/codex/adapter.js +13 -1
- package/dist/providers/codex/cli-client.d.ts +35 -3
- package/dist/providers/codex/cli-client.js +212 -31
- package/dist/providers/codex/project-doc.js +16 -29
- package/dist/providers/copilot/adapter.d.ts +3 -1
- package/dist/providers/copilot/adapter.js +13 -1
- package/dist/providers/copilot/cli-client.d.ts +34 -2
- package/dist/providers/copilot/cli-client.js +196 -18
- package/dist/providers/create-provider.d.ts +1 -1
- package/dist/providers/create-provider.js +30 -14
- package/dist/providers/idle-backend-shutdown.d.ts +16 -0
- package/dist/providers/idle-backend-shutdown.js +53 -0
- 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 +40 -2
- package/package.json +2 -2
- package/skills/borgee-agent/SKILL.md +133 -35
- 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 -507
- package/skills/borgee-agent/borgee-agent.py +0 -438
|
@@ -505,6 +505,23 @@ class LoopbackLocalhostGatewayController {
|
|
|
505
505
|
this.recordAudit('authorized', 200, decision.path, request.method ?? 'GET', decision.binding);
|
|
506
506
|
return;
|
|
507
507
|
}
|
|
508
|
+
case 'current-task-property': {
|
|
509
|
+
const task = await this.loadCurrentThreadTask(binding);
|
|
510
|
+
const updatedTask = await this.applyTaskPropertyChange(request, task.id, decision.route.key);
|
|
511
|
+
this.sendTaskPropertyResult(response, decision, binding.channelId, updatedTask, request.method);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
case 'task-property': {
|
|
515
|
+
// Load first: the write must be refused for a task outside the bound
|
|
516
|
+
// channel BEFORE it lands, not reported as not-found afterwards.
|
|
517
|
+
const task = await this.loadAuthorizedTask(binding.channelId, {
|
|
518
|
+
taskId: decision.route.taskId,
|
|
519
|
+
resource: 'task',
|
|
520
|
+
});
|
|
521
|
+
const updatedTask = await this.applyTaskPropertyChange(request, task.id, decision.route.key);
|
|
522
|
+
this.sendTaskPropertyResult(response, decision, binding.channelId, updatedTask, request.method);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
508
525
|
case 'task-history': {
|
|
509
526
|
// The thread this reads is the one the server recorded on the task: thread_id is
|
|
510
527
|
// written once by task creation and is not part of the task update whitelist, so no
|
|
@@ -612,9 +629,40 @@ class LoopbackLocalhostGatewayController {
|
|
|
612
629
|
}
|
|
613
630
|
return task;
|
|
614
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* PUT writes the key, DELETE removes it. The method has already been
|
|
634
|
+
* narrowed to those two by the route's allowed-method table.
|
|
635
|
+
*/
|
|
636
|
+
async applyTaskPropertyChange(request, taskId, key) {
|
|
637
|
+
try {
|
|
638
|
+
if (request.method === 'DELETE') {
|
|
639
|
+
return await this.controlPlane.deleteTaskProperty({ taskId, key });
|
|
640
|
+
}
|
|
641
|
+
const body = await readGatewayJsonBody(request);
|
|
642
|
+
return await this.controlPlane.setTaskProperty({ taskId, key, value: parseTaskPropertyValue(body) });
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
throw mapGatewayControlPlaneError(error, 'authorized');
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
sendTaskPropertyResult(response, decision, boundChannelId, task, method) {
|
|
649
|
+
if (!isTaskInAuthorizedScope(boundChannelId, task)) {
|
|
650
|
+
this.sendJson(response, 404, { error: 'not_found' });
|
|
651
|
+
this.recordAudit('not-found', 404, decision.path, method ?? 'PUT', decision.binding);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
this.sendJson(response, 200, task);
|
|
655
|
+
this.recordAudit('authorized', 200, decision.path, method ?? 'PUT', decision.binding);
|
|
656
|
+
}
|
|
615
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
|
+
}
|
|
616
664
|
let task;
|
|
617
|
-
const preferredTaskId = binding.payload
|
|
665
|
+
const preferredTaskId = binding.payload.taskAssignmentContext.currentTaskId;
|
|
618
666
|
try {
|
|
619
667
|
task = await findTaskForThread(this.controlPlane, binding.channelId, preferredTaskId);
|
|
620
668
|
}
|
|
@@ -816,6 +864,17 @@ function parseUpdateTaskInput(taskId, body) {
|
|
|
816
864
|
}
|
|
817
865
|
return input;
|
|
818
866
|
}
|
|
867
|
+
/**
|
|
868
|
+
* The value is required and must be a string. An absent or non-string value is
|
|
869
|
+
* a caller bug, not an instruction to store the empty string — storing that
|
|
870
|
+
* would silently blank a key the caller meant to set.
|
|
871
|
+
*/
|
|
872
|
+
function parseTaskPropertyValue(body) {
|
|
873
|
+
if (!isRecord(body) || typeof body.value !== 'string') {
|
|
874
|
+
throw new GatewayHttpError(400, { error: 'invalid_property_value' }, 'bad-request');
|
|
875
|
+
}
|
|
876
|
+
return body.value;
|
|
877
|
+
}
|
|
819
878
|
function isTaskInAuthorizedScope(boundChannelId, task) {
|
|
820
879
|
return task.channelId === boundChannelId || task.threadId === boundChannelId;
|
|
821
880
|
}
|
|
@@ -846,6 +905,21 @@ function mapGatewayControlPlaneError(error, fallbackReason) {
|
|
|
846
905
|
case 'bpp.channel_id_required':
|
|
847
906
|
case 'bpp.task_id_required':
|
|
848
907
|
return new GatewayHttpError(400, { error: 'bad_request' }, 'bad-request');
|
|
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.
|
|
911
|
+
case 'bpp.task_property_key_unknown':
|
|
912
|
+
return new GatewayHttpError(400, { error: 'unknown_property_key' }, 'bad-request');
|
|
913
|
+
case 'bpp.task_property_value_too_long':
|
|
914
|
+
return new GatewayHttpError(400, { error: 'property_value_too_long' }, 'bad-request');
|
|
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.
|
|
921
|
+
case 'bpp.task_property_not_found':
|
|
922
|
+
return new GatewayHttpError(404, { error: 'property_not_found' }, 'not-found');
|
|
849
923
|
default:
|
|
850
924
|
return new GatewayHttpError(502, { error: 'upstream_error' }, fallbackReason);
|
|
851
925
|
}
|
|
@@ -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/local-config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { parseDocument, stringify } from 'yaml';
|
|
5
|
-
import { assertProviderCompatibility, assertProviderCommandCompatibility, optionalNonEmptyString, optionalStringArray, parseCopilotSessionTtlMinutesValue, requireNonEmptyString, resolveProvider, resolveProviderCommandConfig, } from './config.js';
|
|
5
|
+
import { assertProviderCompatibility, assertProviderCommandCompatibility, optionalNonEmptyString, optionalStringArray, parseCopilotSessionTtlMinutesValue, parseProviderIdleShutdownMinutesValue, requireNonEmptyString, resolveProvider, resolveProviderCommandConfig, } from './config.js';
|
|
6
6
|
import { resolveLocalConfigAgentStateRoot } from './state-paths.js';
|
|
7
7
|
const SUPPORTED_CONFIG_EXTENSIONS = new Set(['.json', '.yaml', '.yml']);
|
|
8
8
|
export const DEFAULT_LOCAL_HOST_CONFIG_FILENAME = 'agents-host.yaml';
|
|
@@ -124,6 +124,9 @@ function parseProviderCommandOverrides(value, sourceLabel) {
|
|
|
124
124
|
if (value.copilotSessionTtlMinutes !== undefined && value.copilotSessionTtlMinutes !== null) {
|
|
125
125
|
overrides.copilotSessionTtlMinutes = parseCopilotSessionTtlMinutesValue(value.copilotSessionTtlMinutes, sourceLabel);
|
|
126
126
|
}
|
|
127
|
+
if (value.providerIdleShutdownMinutes !== undefined && value.providerIdleShutdownMinutes !== null) {
|
|
128
|
+
overrides.providerIdleShutdownMinutes = parseProviderIdleShutdownMinutesValue(value.providerIdleShutdownMinutes, sourceLabel);
|
|
129
|
+
}
|
|
127
130
|
return overrides;
|
|
128
131
|
}
|
|
129
132
|
function toAgentsDir(hostConfigPath, rawAgentsDir) {
|
|
@@ -593,6 +596,9 @@ function renderAgentConfigYaml(agent) {
|
|
|
593
596
|
if (agent.copilotSessionTtlMinutes !== undefined) {
|
|
594
597
|
config.copilotSessionTtlMinutes = agent.copilotSessionTtlMinutes;
|
|
595
598
|
}
|
|
599
|
+
if (agent.providerIdleShutdownMinutes !== undefined) {
|
|
600
|
+
config.providerIdleShutdownMinutes = agent.providerIdleShutdownMinutes;
|
|
601
|
+
}
|
|
596
602
|
return stringify(config);
|
|
597
603
|
}
|
|
598
604
|
function buildManagedAgentSnapshot(host, stateRootBaseDir, sourcePath, agent) {
|
|
@@ -629,6 +635,9 @@ function resolveAgentProviderCommandConfig(hostDefaults, agent) {
|
|
|
629
635
|
...(agent.copilotSessionTtlMinutes !== undefined
|
|
630
636
|
? { copilotSessionTtlMinutes: agent.copilotSessionTtlMinutes }
|
|
631
637
|
: {}),
|
|
638
|
+
...(agent.providerIdleShutdownMinutes !== undefined
|
|
639
|
+
? { providerIdleShutdownMinutes: agent.providerIdleShutdownMinutes }
|
|
640
|
+
: {}),
|
|
632
641
|
});
|
|
633
642
|
}
|
|
634
643
|
export async function loadLocalConfigSnapshot(hostConfigPath, deps = {}) {
|
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;
|
|
@@ -339,6 +339,7 @@ export function buildManagedLocalAgentConfig(options) {
|
|
|
339
339
|
copilotCommand: config.copilotCommand,
|
|
340
340
|
copilotArgs: [...config.copilotArgs],
|
|
341
341
|
copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
|
|
342
|
+
providerIdleShutdownMinutes: config.providerIdleShutdownMinutes,
|
|
342
343
|
},
|
|
343
344
|
};
|
|
344
345
|
}
|
|
@@ -382,6 +383,9 @@ function mergeManagedLocalAgentConfig(hostDefaults, existingAgent, generatedAgen
|
|
|
382
383
|
copilotSessionTtlMinutes: hasOverride('COPILOT_SESSION_TTL_MINUTES')
|
|
383
384
|
? generatedAgent.copilotSessionTtlMinutes
|
|
384
385
|
: existingAgent.copilotSessionTtlMinutes,
|
|
386
|
+
providerIdleShutdownMinutes: hasOverride('PROVIDER_IDLE_SHUTDOWN_MINUTES')
|
|
387
|
+
? generatedAgent.providerIdleShutdownMinutes
|
|
388
|
+
: existingAgent.providerIdleShutdownMinutes,
|
|
385
389
|
};
|
|
386
390
|
}
|
|
387
391
|
function collectExplicitManagedAgentEnvOverrides(processEnv, optionEnv) {
|
|
@@ -396,6 +400,7 @@ function collectExplicitManagedAgentEnvOverrides(processEnv, optionEnv) {
|
|
|
396
400
|
'COPILOT_COMMAND',
|
|
397
401
|
'COPILOT_ARGS',
|
|
398
402
|
'COPILOT_SESSION_TTL_MINUTES',
|
|
403
|
+
'PROVIDER_IDLE_SHUTDOWN_MINUTES',
|
|
399
404
|
]) {
|
|
400
405
|
if (processEnv[key] !== undefined) {
|
|
401
406
|
explicitOverrides[key] = processEnv[key];
|
|
@@ -594,8 +599,11 @@ async function canConnectToSocket(socketPath) {
|
|
|
594
599
|
socket.once('error', () => finalize(false));
|
|
595
600
|
});
|
|
596
601
|
}
|
|
597
|
-
async function removeStaleControlSocket(
|
|
598
|
-
|
|
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) => {
|
|
599
607
|
if (isNotFoundError(error)) {
|
|
600
608
|
return undefined;
|
|
601
609
|
}
|
|
@@ -605,15 +613,15 @@ async function removeStaleControlSocket(socketPath) {
|
|
|
605
613
|
return false;
|
|
606
614
|
}
|
|
607
615
|
if (stats.isDirectory() || stats.isSymbolicLink()) {
|
|
608
|
-
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}`);
|
|
609
617
|
}
|
|
610
|
-
if (await canConnectToSocket(
|
|
618
|
+
if (await canConnectToSocket(endpoint.address)) {
|
|
611
619
|
return false;
|
|
612
620
|
}
|
|
613
621
|
if (!stats.isSocket() && !stats.isFile()) {
|
|
614
|
-
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}`);
|
|
615
623
|
}
|
|
616
|
-
await fs.rm(
|
|
624
|
+
await fs.rm(endpoint.address, { force: true });
|
|
617
625
|
return true;
|
|
618
626
|
}
|
|
619
627
|
function parseDaemonResponse(raw) {
|
|
@@ -650,12 +658,12 @@ function isManagedDaemonConnectionError(error) {
|
|
|
650
658
|
isNodeErrorWithCode(error, 'ECONNRESET'));
|
|
651
659
|
}
|
|
652
660
|
export async function sendManagedDaemonRequest(rootPath, request) {
|
|
653
|
-
const
|
|
661
|
+
const endpoint = resolveManagedDaemonEndpoint(rootPath);
|
|
654
662
|
const requestTimeoutMs = request.type === 'upsertAgent' || request.type === 'applySpec'
|
|
655
663
|
? DAEMON_RECONCILE_WAIT_MS
|
|
656
664
|
: DAEMON_READY_WAIT_MS;
|
|
657
665
|
return new Promise((resolveRequest, rejectRequest) => {
|
|
658
|
-
const socket = createConnection(
|
|
666
|
+
const socket = createConnection(endpoint.address);
|
|
659
667
|
let settled = false;
|
|
660
668
|
let responseBuffer = '';
|
|
661
669
|
const rejectOnce = (error) => {
|
|
@@ -676,7 +684,7 @@ export async function sendManagedDaemonRequest(rootPath, request) {
|
|
|
676
684
|
};
|
|
677
685
|
socket.setEncoding('utf8');
|
|
678
686
|
socket.setTimeout(requestTimeoutMs);
|
|
679
|
-
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}`)));
|
|
680
688
|
socket.once('error', rejectOnce);
|
|
681
689
|
socket.on('data', (chunk) => {
|
|
682
690
|
responseBuffer += chunk;
|
|
@@ -785,9 +793,9 @@ async function readReadyManagedDaemonStatus(rootPath, sendRequest) {
|
|
|
785
793
|
}
|
|
786
794
|
async function waitForManagedDaemonShutdown(rootPath, canConnect) {
|
|
787
795
|
const deadline = Date.now() + DAEMON_READY_WAIT_MS;
|
|
788
|
-
const
|
|
796
|
+
const endpoint = resolveManagedDaemonEndpoint(rootPath);
|
|
789
797
|
while (Date.now() < deadline) {
|
|
790
|
-
const listening = await canConnect(
|
|
798
|
+
const listening = await canConnect(endpoint.address).catch((error) => {
|
|
791
799
|
if (isNodeErrorWithCode(error, 'ENOENT')) {
|
|
792
800
|
return false;
|
|
793
801
|
}
|
|
@@ -1476,7 +1484,7 @@ export async function applyManagedSpec(options, deps = {}) {
|
|
|
1476
1484
|
export class ManagedAgentsHostDaemon {
|
|
1477
1485
|
rootPath;
|
|
1478
1486
|
hostConfigPath;
|
|
1479
|
-
|
|
1487
|
+
endpoint;
|
|
1480
1488
|
createSupervisor;
|
|
1481
1489
|
supervisor;
|
|
1482
1490
|
loadSpec;
|
|
@@ -1488,14 +1496,14 @@ export class ManagedAgentsHostDaemon {
|
|
|
1488
1496
|
requestQueue = Promise.resolve();
|
|
1489
1497
|
started = false;
|
|
1490
1498
|
ready = false;
|
|
1491
|
-
|
|
1499
|
+
ownsEndpoint = false;
|
|
1492
1500
|
shutdownRequested = false;
|
|
1493
1501
|
ownedSocketInode = null;
|
|
1494
1502
|
constructor(rootPath, debug = false, deps = {}) {
|
|
1495
1503
|
const layout = resolveLocalConfigLayout(rootPath);
|
|
1496
1504
|
this.rootPath = layout.root;
|
|
1497
1505
|
this.hostConfigPath = layout.hostConfigPath;
|
|
1498
|
-
this.
|
|
1506
|
+
this.endpoint = resolveManagedDaemonEndpoint(this.rootPath, deps.platform);
|
|
1499
1507
|
this.createSupervisor =
|
|
1500
1508
|
deps.createSupervisor ??
|
|
1501
1509
|
((configPath, daemonDebug) => new AgentsHostSupervisor(configPath, { debug: daemonDebug }));
|
|
@@ -1530,22 +1538,34 @@ export class ManagedAgentsHostDaemon {
|
|
|
1530
1538
|
this.logger.log('[agents-host] managed daemon ready', {
|
|
1531
1539
|
rootPath: this.rootPath,
|
|
1532
1540
|
hostConfigPath: this.hostConfigPath,
|
|
1533
|
-
socketPath: this.
|
|
1541
|
+
socketPath: this.endpoint.address,
|
|
1534
1542
|
});
|
|
1535
1543
|
}
|
|
1536
1544
|
catch (error) {
|
|
1537
1545
|
this.started = false;
|
|
1538
1546
|
this.ready = false;
|
|
1539
|
-
|
|
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
|
+
}
|
|
1540
1556
|
throw error;
|
|
1541
1557
|
}
|
|
1542
1558
|
}
|
|
1543
1559
|
async stop() {
|
|
1544
1560
|
this.ready = false;
|
|
1561
|
+
let stopError;
|
|
1545
1562
|
try {
|
|
1546
1563
|
await this.supervisor.stop();
|
|
1547
1564
|
}
|
|
1548
|
-
|
|
1565
|
+
catch (error) {
|
|
1566
|
+
stopError = error;
|
|
1567
|
+
}
|
|
1568
|
+
try {
|
|
1549
1569
|
await new Promise((resolveStop) => {
|
|
1550
1570
|
if (!this.server.listening) {
|
|
1551
1571
|
resolveStop();
|
|
@@ -1553,17 +1573,38 @@ export class ManagedAgentsHostDaemon {
|
|
|
1553
1573
|
}
|
|
1554
1574
|
this.server.close(() => resolveStop());
|
|
1555
1575
|
});
|
|
1556
|
-
if (this.
|
|
1557
|
-
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
|
+
});
|
|
1558
1583
|
if (socketStats && socketStats.ino === this.ownedSocketInode) {
|
|
1559
|
-
await fs.rm(this.
|
|
1584
|
+
await fs.rm(this.endpoint.address, { force: true });
|
|
1560
1585
|
}
|
|
1561
|
-
this.ownsSocket = false;
|
|
1562
|
-
this.ownedSocketInode = null;
|
|
1563
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;
|
|
1564
1602
|
this.started = false;
|
|
1565
1603
|
this.shutdownRequested = false;
|
|
1566
1604
|
}
|
|
1605
|
+
if (stopError) {
|
|
1606
|
+
throw stopError;
|
|
1607
|
+
}
|
|
1567
1608
|
}
|
|
1568
1609
|
async listen() {
|
|
1569
1610
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
@@ -1579,20 +1620,32 @@ export class ManagedAgentsHostDaemon {
|
|
|
1579
1620
|
};
|
|
1580
1621
|
this.server.once('error', onError);
|
|
1581
1622
|
this.server.once('listening', onListening);
|
|
1582
|
-
this.server.listen(this.
|
|
1623
|
+
this.server.listen(this.endpoint.address);
|
|
1583
1624
|
});
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
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;
|
|
1587
1630
|
return;
|
|
1588
1631
|
}
|
|
1589
1632
|
catch (error) {
|
|
1590
|
-
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))) {
|
|
1591
1644
|
throw error;
|
|
1592
1645
|
}
|
|
1593
1646
|
}
|
|
1594
1647
|
}
|
|
1595
|
-
throw new Error(`Unable to bind managed daemon control
|
|
1648
|
+
throw new Error(`Unable to bind managed daemon control endpoint: ${this.endpoint.address}`);
|
|
1596
1649
|
}
|
|
1597
1650
|
async handleSocket(socket) {
|
|
1598
1651
|
let buffer = '';
|
package/dist/plugin-sdk.js
CHANGED
|
@@ -4729,6 +4729,12 @@ function encodeActionPayload(action) {
|
|
|
4729
4729
|
...action.heartbeat_interval_ms !== void 0 ? { heartbeat_interval_ms: action.heartbeat_interval_ms } : {},
|
|
4730
4730
|
...action.heartbeat_prompt !== void 0 ? { heartbeat_prompt: action.heartbeat_prompt } : {}
|
|
4731
4731
|
};
|
|
4732
|
+
case "set_task_property":
|
|
4733
|
+
return { task_id: action.taskId, key: action.key, value: action.value };
|
|
4734
|
+
case "delete_task_property":
|
|
4735
|
+
return { task_id: action.taskId, key: action.key };
|
|
4736
|
+
case "list_task_property_definitions":
|
|
4737
|
+
return {};
|
|
4732
4738
|
default:
|
|
4733
4739
|
return {};
|
|
4734
4740
|
}
|
|
@@ -4795,7 +4801,17 @@ function decodeActionResult(op, payloadJSON, frameCursor) {
|
|
|
4795
4801
|
case "create_task":
|
|
4796
4802
|
case "get_task":
|
|
4797
4803
|
case "update_task":
|
|
4804
|
+
case "set_task_property":
|
|
4805
|
+
case "delete_task_property":
|
|
4798
4806
|
return decodeTask(p);
|
|
4807
|
+
case "list_task_property_definitions": {
|
|
4808
|
+
const arr = Array.isArray(p) ? p : [];
|
|
4809
|
+
return arr.map((d) => ({
|
|
4810
|
+
key: String(d.key ?? ""),
|
|
4811
|
+
label: String(d.label ?? ""),
|
|
4812
|
+
render: d.render === "url" || d.render === "identifier" ? d.render : "text"
|
|
4813
|
+
}));
|
|
4814
|
+
}
|
|
4799
4815
|
case "list_tasks": {
|
|
4800
4816
|
const arr = Array.isArray(p) ? p : [];
|
|
4801
4817
|
return arr.map(decodeTask);
|
|
@@ -4809,6 +4825,7 @@ function decodeTask(t) {
|
|
|
4809
4825
|
id: String(t.id ?? ""),
|
|
4810
4826
|
channelId: String(t.channel_id ?? ""),
|
|
4811
4827
|
guildId: String(t.guild_id ?? ""),
|
|
4828
|
+
seq: Number(t.seq ?? 0),
|
|
4812
4829
|
title: String(t.title ?? ""),
|
|
4813
4830
|
description: String(t.description ?? ""),
|
|
4814
4831
|
status: String(t.status ?? ""),
|
|
@@ -4818,9 +4835,23 @@ function decodeTask(t) {
|
|
|
4818
4835
|
messageId: t.message_id != null ? String(t.message_id) : null,
|
|
4819
4836
|
threadId: t.thread_id != null ? String(t.thread_id) : null,
|
|
4820
4837
|
createdAt: Number(t.created_at ?? 0),
|
|
4821
|
-
updatedAt: Number(t.updated_at ?? 0)
|
|
4838
|
+
updatedAt: Number(t.updated_at ?? 0),
|
|
4839
|
+
heartbeatIntervalMs: Number(t.heartbeat_interval_ms ?? 0),
|
|
4840
|
+
heartbeatPrompt: String(t.heartbeat_prompt ?? ""),
|
|
4841
|
+
participants: Array.isArray(t.participants) ? t.participants.map((p) => String(p)) : [],
|
|
4842
|
+
properties: decodeTaskProperties(t.properties)
|
|
4822
4843
|
};
|
|
4823
4844
|
}
|
|
4845
|
+
function decodeTaskProperties(raw) {
|
|
4846
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw))
|
|
4847
|
+
return {};
|
|
4848
|
+
const out = {};
|
|
4849
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
4850
|
+
if (typeof value === "string")
|
|
4851
|
+
out[key] = value;
|
|
4852
|
+
}
|
|
4853
|
+
return out;
|
|
4854
|
+
}
|
|
4824
4855
|
function mapInbound(frame) {
|
|
4825
4856
|
const kind = frame.kind;
|
|
4826
4857
|
const base = {
|
|
@@ -5068,6 +5099,32 @@ var Client = class {
|
|
|
5068
5099
|
heartbeat_prompt: input.heartbeatPrompt
|
|
5069
5100
|
});
|
|
5070
5101
|
}
|
|
5102
|
+
// ─── Task properties ────────────────────────────────────
|
|
5103
|
+
// One key per call. The server has no whole-object property write, so two
|
|
5104
|
+
// agents writing different keys of the same task never overwrite each other.
|
|
5105
|
+
async setTaskProperty(input) {
|
|
5106
|
+
return await this.t.perform({
|
|
5107
|
+
op: "set_task_property",
|
|
5108
|
+
taskId: input.taskId,
|
|
5109
|
+
key: input.key,
|
|
5110
|
+
value: input.value
|
|
5111
|
+
});
|
|
5112
|
+
}
|
|
5113
|
+
async deleteTaskProperty(input) {
|
|
5114
|
+
return await this.t.perform({
|
|
5115
|
+
op: "delete_task_property",
|
|
5116
|
+
taskId: input.taskId,
|
|
5117
|
+
key: input.key
|
|
5118
|
+
});
|
|
5119
|
+
}
|
|
5120
|
+
/**
|
|
5121
|
+
* The registered property keys with their labels and render kinds. Read this
|
|
5122
|
+
* rather than hardcoding keys — the server can register a new property
|
|
5123
|
+
* without an SDK release.
|
|
5124
|
+
*/
|
|
5125
|
+
async listTaskPropertyDefinitions() {
|
|
5126
|
+
return await this.t.perform({ op: "list_task_property_definitions" });
|
|
5127
|
+
}
|
|
5071
5128
|
startTyping(channelId) {
|
|
5072
5129
|
const emit = () => {
|
|
5073
5130
|
void this.t.perform({ op: "report_typing", channelId }).catch((err) => this.logger.warn("startTyping failed", err));
|