@borgee/agents-host 0.2.33 → 0.2.35
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 +17 -0
- package/dist/agents-host.js +52 -0
- 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 +6 -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/main-session-delegation.d.ts +1 -0
- package/dist/context/main-session-delegation.js +6 -0
- package/dist/context/prompt.js +4 -2
- package/dist/gateway/localhost-gateway.js +66 -0
- package/dist/local-config.js +10 -1
- package/dist/managed-daemon.js +5 -0
- 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.js +3 -1
- package/dist/providers/claude/cli-client.d.ts +25 -1
- package/dist/providers/claude/cli-client.js +127 -10
- package/dist/providers/codex/adapter.js +3 -1
- package/dist/providers/codex/cli-client.d.ts +25 -1
- package/dist/providers/codex/cli-client.js +127 -10
- package/dist/providers/codex/project-doc.js +3 -0
- package/dist/providers/copilot/adapter.js +3 -1
- package/dist/providers/copilot/cli-client.d.ts +25 -1
- package/dist/providers/copilot/cli-client.js +125 -10
- package/dist/providers/create-provider.js +14 -9
- package/dist/providers/idle-backend-shutdown.d.ts +16 -0
- package/dist/providers/idle-backend-shutdown.js +53 -0
- package/dist/types.d.ts +7 -0
- package/package.json +2 -2
- package/skills/borgee-agent/SKILL.md +19 -2
- package/skills/borgee-agent/borgee-agent.mjs +56 -1
- package/skills/borgee-agent/borgee-agent.py +33 -2
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ The current shipped internal compatibility surface resolves gates in this order:
|
|
|
89
89
|
2. add any explicit entries from `AGENTS_HOST_INTERNAL_COMPATIBILITY_GATES`
|
|
90
90
|
3. subtract any explicit entries from `AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES`
|
|
91
91
|
|
|
92
|
-
The shipped default set now also includes `connections-state-layer`, `context-injection`, `skill-runtime`, `localhost-gateway`, `token-binding`, `collaboration-outcome-model`, `task-thread-collaboration-contract`,
|
|
92
|
+
The shipped default set now also includes `connections-state-layer`, `context-injection`, `skill-runtime`, `localhost-gateway`, `token-binding`, `collaboration-outcome-model`, `task-thread-collaboration-contract`, `collaboration-capabilities-diagnostics`, and `provider-idle-shutdown`. `attention-follow-semantics`, `collaboration-skill-first`, and hosted `praestoclaw-provider` still remain opt-in.
|
|
93
93
|
|
|
94
94
|
Managed-runtime settings still preserve the managed rollback rule for `connections-state-layer` plus `token-binding`: when `managed-runtime-convergence` remains enabled, the managed surface forces that pair on; explicitly disabling `managed-runtime-convergence` removes that pair from the managed surface again unless you also explicitly re-enable them.
|
|
95
95
|
|
|
@@ -101,6 +101,7 @@ Rollback and hardening controls stay explicit:
|
|
|
101
101
|
- `AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES=<comma-list>` disables shipped gates for rollback
|
|
102
102
|
- `AGENTS_HOST_INTERNAL_PROVIDER_IMPLEMENTATIONS=copilot:v1` forces Copilot back to the v1 adapter while its v2 gate stays enabled; stale `claude:v1|v2` entries are tolerated as no-ops for compatibility
|
|
103
103
|
- `AGENTS_HOST_INTERNAL_POLICY_MODE=enforce` hardens the shipped `policy-audit-enforcement` gate beyond its default `audit-only` mode
|
|
104
|
+
- `AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES=provider-idle-shutdown` keeps every provider's shared ACP adapter process resident again; `PROVIDER_IDLE_SHUTDOWN_MINUTES=0` does the same without touching gates
|
|
104
105
|
|
|
105
106
|
Or with the CLI:
|
|
106
107
|
|
|
@@ -240,6 +241,9 @@ Notes:
|
|
|
240
241
|
| `CODEX_COMMAND` / `CODEX_ARGS` | `--codex-command` / `--codex-args` | no | `codex-acp` / empty | Local Codex ACP adapter command + args. The default launch resolves the bundled `@agentclientprotocol/codex-acp` entrypoint. |
|
|
241
242
|
| `COPILOT_COMMAND` / `COPILOT_ARGS` | `--copilot-command` / `--copilot-args` | no | `copilot` / parsed but ignored by ACP runtime | Local Copilot CLI command. The persistent Copilot ACP prototype always launches `copilot --acp`. |
|
|
242
243
|
| `COPILOT_SESSION_TTL_MINUTES` | `--copilot-session-ttl-minutes` | no | `2880` | Idle TTL for per-channel Copilot ACP sessions |
|
|
244
|
+
| `PROVIDER_IDLE_SHUTDOWN_MINUTES` | `--provider-idle-shutdown-minutes` | no | `10` | Idle window before the agent's shared ACP adapter **process** is shut down and re-spawned on the next turn. Applies to Claude, Codex, and Copilot. `0` keeps the process resident. Must be non-negative and no larger than the Node.js timer limit (~35791.39 minutes); anything else is rejected at startup. |
|
|
245
|
+
|
|
246
|
+
`COPILOT_SESSION_TTL_MINUTES` and `PROVIDER_IDLE_SHUTDOWN_MINUTES` are two different layers and do not replace each other. The first evicts an individual idle Copilot ACP *session* inside a running adapter process. The second shuts the shared adapter *process* itself down once every channel it hosts is idle, for all three providers, and takes every live session in that process with it. See [Conversation memory](#conversation-memory) for what happens to those sessions afterwards.
|
|
243
247
|
|
|
244
248
|
## Local-config mode
|
|
245
249
|
|
|
@@ -325,7 +329,8 @@ The spec shape is:
|
|
|
325
329
|
"codexArgs": [],
|
|
326
330
|
"copilotCommand": "copilot",
|
|
327
331
|
"copilotArgs": ["-s", "--no-color", "--allow-all-tools", "--output-format", "text"],
|
|
328
|
-
"copilotSessionTtlMinutes": 2880
|
|
332
|
+
"copilotSessionTtlMinutes": 2880,
|
|
333
|
+
"providerIdleShutdownMinutes": 10
|
|
329
334
|
}
|
|
330
335
|
},
|
|
331
336
|
"agents": [
|
|
@@ -398,6 +403,7 @@ defaults:
|
|
|
398
403
|
- --output-format
|
|
399
404
|
- text
|
|
400
405
|
copilotSessionTtlMinutes: 2880
|
|
406
|
+
providerIdleShutdownMinutes: 10
|
|
401
407
|
```
|
|
402
408
|
|
|
403
409
|
Host config fields:
|
|
@@ -411,6 +417,7 @@ Host config fields:
|
|
|
411
417
|
- `defaults.copilotCommand`
|
|
412
418
|
- `defaults.copilotArgs`
|
|
413
419
|
- `defaults.copilotSessionTtlMinutes`
|
|
420
|
+
- `defaults.providerIdleShutdownMinutes`
|
|
414
421
|
|
|
415
422
|
Absent host defaults fall back to the same code-level defaults used by the
|
|
416
423
|
standalone env flow, foreground `start`, and `start-managed`.
|
|
@@ -434,8 +441,8 @@ Agent config fields:
|
|
|
434
441
|
- `enabled` (optional, default `true`)
|
|
435
442
|
- `claudeCommand` / `claudeArgs` (optional): leave unset to use the shipped bundled `claude-agent-acp` path. On that bundled path `claudeArgs` must stay empty. Non-empty `claudeArgs` are only supported when `claudeCommand` points at a custom ACP adapter command.
|
|
436
443
|
- optional overrides for `claudeCommand`, `claudeArgs`, `codexCommand`,
|
|
437
|
-
`codexArgs`, `copilotCommand`, `copilotArgs`,
|
|
438
|
-
`copilotSessionTtlMinutes`
|
|
444
|
+
`codexArgs`, `copilotCommand`, `copilotArgs`,
|
|
445
|
+
`copilotSessionTtlMinutes`, and `providerIdleShutdownMinutes`
|
|
439
446
|
|
|
440
447
|
Array overrides are **replacement**, not concatenation. For example, an agent
|
|
441
448
|
`claudeArgs` value replaces the host default `claudeArgs` entirely.
|
|
@@ -504,9 +511,23 @@ active:
|
|
|
504
511
|
state root and restores same-host continuity with ACP `session/resume` when
|
|
505
512
|
available, otherwise `session/load` plus local replay cleanup on runtimes
|
|
506
513
|
that advertise only load support.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
514
|
+
|
|
515
|
+
We keep no transcript or history store of our own — only the native session id each provider should try to reopen next time. The transcripts themselves live in each CLI's own on-disk session store: `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl` for Claude, `~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-<timestamp>-<threadId>.jsonl` for Codex, and `~/.copilot/session-state/<guid>/` for Copilot. Because those stores are on disk rather than in the adapter process, a session can be reopened later by a *different* process on the same machine — which is what makes conversation memory survive both a host restart and the idle recycle below. Continuity is same-machine and same-`$HOME` only: another machine, or a different home directory, has no session to reopen.
|
|
516
|
+
|
|
517
|
+
### Idle adapter shutdown
|
|
518
|
+
|
|
519
|
+
Each of those subprocesses is shut down once every channel it hosts has been idle for `PROVIDER_IDLE_SHUTDOWN_MINUTES` (default `10`), and a fresh one is spawned lazily on the next turn. "Idle" means the host has no active turn, no queued turn, no ACP session start in flight, and nothing still being processed for any channel of that agent; a turn that arrives mid-shutdown waits for the teardown and then starts the replacement rather than failing. Only the provider subprocess is recycled — the agent stays connected to Borgee, the loopback gateway keeps running, and the agents-host process itself is untouched.
|
|
520
|
+
|
|
521
|
+
Conversation memory normally survives the recycle. Before the process goes away the host closes each live ACP session, and on the next turn it reopens the persisted session id against the CLI's on-disk store — Claude and Codex through ACP `session/resume`, Copilot through `session/load` (a sequential resume rather than a live re-sync; the replayed history is discarded locally instead of being re-posted to the channel). This is the same restore path a host restart takes, and its ordinary outcome is that the model still remembers the conversation.
|
|
522
|
+
|
|
523
|
+
When a restore does fail, the run does not break: the stale session id is dropped, a fresh `session/new` starts, and the turn still answers coherently because the host re-materializes the per-channel `context.json` and re-injects the same bootstrap context on **every** turn regardless. What is lost in that case is only the CLI-native transcript. The situations that actually cause it:
|
|
524
|
+
|
|
525
|
+
- **A different machine or a different `$HOME`.** The session store is local, so continuity never crosses hosts.
|
|
526
|
+
- **A stale Copilot session lock.** The Copilot CLI does not reliably unlink its `inuse.<pid>.lock` on an unclean exit ([github/copilot-cli#3255](https://github.com/github/copilot-cli/issues/3255), still open), so a force-killed process can leave a lock that makes the next restore believe the session is still in use. The idle teardown mitigates this: it gives the CLI a 5-second SIGTERM grace before any SIGKILL — twenty times the grace a fatal teardown uses — precisely so the CLI has time to unlink that lock. This is an internal default, not a knob.
|
|
527
|
+
- **A very large Copilot transcript.** Long-running channels can hit upstream resume bugs: slow or memory-hungry resume ([#4251](https://github.com/github/copilot-cli/issues/4251)), a session that becomes unloadable once `events.jsonl` outgrows the V8 maximum string length ([#4325](https://github.com/github/copilot-cli/issues/4325)), and a resume-triggered compaction hang ([#4138](https://github.com/github/copilot-cli/issues/4138)). A Copilot resume may also reset the model back to the default ([#4397](https://github.com/github/copilot-cli/issues/4397)).
|
|
528
|
+
- **Our own mapping rules, which are stricter than the CLI's.** We only reuse a persisted session id when the session's working directory *and* the set of directories exposed to it still match. So an ordinary channel turn and an eligible `task_assignment` turn — which runs in its own task workspace — deliberately do not share a session, and the switch between them starts a fresh one even though the CLI could still load the old session.
|
|
529
|
+
|
|
530
|
+
If you would rather keep the adapter process resident than take any of that, set `PROVIDER_IDLE_SHUTDOWN_MINUTES=0`, raise the window, or roll the behavior back entirely with `AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES=provider-idle-shutdown`.
|
|
510
531
|
|
|
511
532
|
## Staying up to date
|
|
512
533
|
|
package/dist/agents-host.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export declare class AgentsHost {
|
|
|
21
21
|
private readonly activeTurns;
|
|
22
22
|
private readonly awaitingUserByChannel;
|
|
23
23
|
private readonly collaborationOutcomeByChannel;
|
|
24
|
+
/**
|
|
25
|
+
* The session id last recorded per task, so a multi-turn task writes the
|
|
26
|
+
* property once rather than on every turn that reuses the same session.
|
|
27
|
+
*/
|
|
28
|
+
private readonly recordedAgentSessionByTask;
|
|
24
29
|
private readonly attentionSnapshotByChannel;
|
|
25
30
|
private readonly missedCollaborationDiagnosticByChannel;
|
|
26
31
|
private readonly progressChannelQueues;
|
|
@@ -93,6 +98,18 @@ export declare class AgentsHost {
|
|
|
93
98
|
private confirmProtocolMessage;
|
|
94
99
|
private resolveAttentionStatePath;
|
|
95
100
|
private buildDefaultAttentionSnapshot;
|
|
101
|
+
/**
|
|
102
|
+
* Record which provider session worked this task, as agent.session_id.
|
|
103
|
+
*
|
|
104
|
+
* agents-host writes it rather than the agent: the session id is runtime
|
|
105
|
+
* state the agent has no reliable view of, so asking it to report its own
|
|
106
|
+
* would make the association depend on the agent remembering to.
|
|
107
|
+
*
|
|
108
|
+
* Best-effort and off the turn's critical path — a task whose session id
|
|
109
|
+
* failed to record is still a task that ran, so a failure here must neither
|
|
110
|
+
* fail the turn nor delay it.
|
|
111
|
+
*/
|
|
112
|
+
private recordAgentSessionOnTask;
|
|
96
113
|
private buildTurnTaskThreadContext;
|
|
97
114
|
private ensureAttentionSnapshotLoaded;
|
|
98
115
|
private revalidateAttentionSnapshotForTurn;
|
package/dist/agents-host.js
CHANGED
|
@@ -135,6 +135,7 @@ class DefaultHostRuntime {
|
|
|
135
135
|
copilotCommand: config.copilotCommand,
|
|
136
136
|
copilotArgs: config.copilotArgs,
|
|
137
137
|
copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
|
|
138
|
+
providerIdleShutdownMinutes: config.providerIdleShutdownMinutes,
|
|
138
139
|
}, deps.logger, {
|
|
139
140
|
compatibilityGates,
|
|
140
141
|
authorizationAuditSink: this.authorizationAuditSink,
|
|
@@ -361,6 +362,11 @@ class DraftMessageController {
|
|
|
361
362
|
return this.writeChain;
|
|
362
363
|
}
|
|
363
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* The registered task property agents-host writes to record which provider
|
|
367
|
+
* session worked a task. It matches the server's registry key.
|
|
368
|
+
*/
|
|
369
|
+
const AGENT_SESSION_TASK_PROPERTY_KEY = 'agent.session_id';
|
|
364
370
|
const BLOCKED_AUTHOR_REFRESH_BACKOFF_MS = 1_000;
|
|
365
371
|
const MAX_QUEUED_MESSAGES_PER_CHANNEL = 8;
|
|
366
372
|
const COLLABORATION_SENDS_PER_TURN = 1;
|
|
@@ -383,6 +389,11 @@ export class AgentsHost {
|
|
|
383
389
|
activeTurns = new Set();
|
|
384
390
|
awaitingUserByChannel = new Map();
|
|
385
391
|
collaborationOutcomeByChannel = new Map();
|
|
392
|
+
/**
|
|
393
|
+
* The session id last recorded per task, so a multi-turn task writes the
|
|
394
|
+
* property once rather than on every turn that reuses the same session.
|
|
395
|
+
*/
|
|
396
|
+
recordedAgentSessionByTask = new Map();
|
|
386
397
|
attentionSnapshotByChannel = new Map();
|
|
387
398
|
missedCollaborationDiagnosticByChannel = new Map();
|
|
388
399
|
progressChannelQueues = new Map();
|
|
@@ -504,6 +515,7 @@ export class AgentsHost {
|
|
|
504
515
|
this.collaborationOutcomeByChannel.clear();
|
|
505
516
|
this.attentionSnapshotByChannel.clear();
|
|
506
517
|
this.missedCollaborationDiagnosticByChannel.clear();
|
|
518
|
+
this.recordedAgentSessionByTask.clear();
|
|
507
519
|
this.controlPlaneClosed = true;
|
|
508
520
|
await this.runtime.stop();
|
|
509
521
|
await Promise.allSettled([...this.activeTurns]);
|
|
@@ -1730,6 +1742,41 @@ export class AgentsHost {
|
|
|
1730
1742
|
turnExecutionId: options?.turnExecutionId,
|
|
1731
1743
|
});
|
|
1732
1744
|
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Record which provider session worked this task, as agent.session_id.
|
|
1747
|
+
*
|
|
1748
|
+
* agents-host writes it rather than the agent: the session id is runtime
|
|
1749
|
+
* state the agent has no reliable view of, so asking it to report its own
|
|
1750
|
+
* would make the association depend on the agent remembering to.
|
|
1751
|
+
*
|
|
1752
|
+
* Best-effort and off the turn's critical path — a task whose session id
|
|
1753
|
+
* failed to record is still a task that ran, so a failure here must neither
|
|
1754
|
+
* fail the turn nor delay it.
|
|
1755
|
+
*/
|
|
1756
|
+
async recordAgentSessionOnTask(channelId, taskThreadContext, sessionId) {
|
|
1757
|
+
const taskId = taskThreadContext?.state === 'active' ? taskThreadContext.currentTaskId : undefined;
|
|
1758
|
+
if (!taskId || !sessionId) {
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (this.recordedAgentSessionByTask.get(taskId) === sessionId) {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
try {
|
|
1765
|
+
await this.borgee.setTaskProperty({
|
|
1766
|
+
taskId,
|
|
1767
|
+
key: AGENT_SESSION_TASK_PROPERTY_KEY,
|
|
1768
|
+
value: sessionId,
|
|
1769
|
+
});
|
|
1770
|
+
this.recordedAgentSessionByTask.set(taskId, sessionId);
|
|
1771
|
+
}
|
|
1772
|
+
catch (error) {
|
|
1773
|
+
this.logger.debugError('best-effort agent session property write failed', {
|
|
1774
|
+
channelId,
|
|
1775
|
+
taskId,
|
|
1776
|
+
error,
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1733
1780
|
async buildTurnTaskThreadContext(msg, channelId) {
|
|
1734
1781
|
if (msg.message_type === 'task_assignment') {
|
|
1735
1782
|
const currentTaskId = extractTaskIdFromTaskAssignmentContent(String(msg.content ?? msg.body ?? ''));
|
|
@@ -2126,6 +2173,11 @@ export class AgentsHost {
|
|
|
2126
2173
|
: undefined);
|
|
2127
2174
|
publicReplyText = resolvePublicReplyText(reply);
|
|
2128
2175
|
visibleReplyBody = this.resolveVisibleTurnBody(activeTurn, reply, publicReplyText);
|
|
2176
|
+
// NOT awaited: nothing downstream reads the result, and awaiting it here
|
|
2177
|
+
// widens the window before the staleness check below, where a slow write
|
|
2178
|
+
// can get an already-finished reply discarded. The method swallows its own
|
|
2179
|
+
// errors, so there is nothing for a caller to handle.
|
|
2180
|
+
void this.recordAgentSessionOnTask(channelId, taskThreadContext, reply.sessionId);
|
|
2129
2181
|
const appliedAttentionSnapshot = this.applyAttentionUpdate(channelId, attentionSnapshot, reply.controlMalformed
|
|
2130
2182
|
? undefined
|
|
2131
2183
|
: (reply.control && 'attentionUpdate' in reply.control
|
|
@@ -18,4 +18,13 @@ export interface ChatControlPlane {
|
|
|
18
18
|
taskId: string;
|
|
19
19
|
}): Promise<Task>;
|
|
20
20
|
updateTask(input: UpdateTaskInput): Promise<Task>;
|
|
21
|
+
setTaskProperty(input: {
|
|
22
|
+
taskId: string;
|
|
23
|
+
key: string;
|
|
24
|
+
value: string;
|
|
25
|
+
}): Promise<Task>;
|
|
26
|
+
deleteTaskProperty(input: {
|
|
27
|
+
taskId: string;
|
|
28
|
+
key: string;
|
|
29
|
+
}): Promise<Task>;
|
|
21
30
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type BorgeePluginClient, type BorgeePluginOptions, type InboundMessageEvent } from '../plugin-sdk.js';
|
|
2
2
|
import type { ChannelSummary, ChannelHistoryEntry, ChannelMessageEvent, CreateTaskInput, DirectoryUser, MeResponseUser, PostMessageInput, PostedMessage, ReadChannelHistoryInput, Task, UpdateTaskInput } from '../types.js';
|
|
3
3
|
import type { ChatControlPlane } from './chat-control-plane.js';
|
|
4
|
-
type PluginClientLike = Pick<BorgeePluginClient, 'agentId' | 'close' | 'connect' | 'createTask' | 'deleteMessage' | 'editMessage' | 'getMe' | 'getTask' | 'listChannels' | 'listTasks' | 'listUsers' | 'on' | 'readHistory' | 'sendMessage' | 'startTyping' | 'updateTask'>;
|
|
4
|
+
type PluginClientLike = Pick<BorgeePluginClient, 'agentId' | 'close' | 'connect' | 'createTask' | 'deleteMessage' | 'editMessage' | 'getMe' | 'getTask' | 'listChannels' | 'listTasks' | 'listUsers' | 'on' | 'readHistory' | 'sendMessage' | 'startTyping' | 'updateTask' | 'setTaskProperty' | 'deleteTaskProperty'>;
|
|
5
5
|
type PluginClientFactory = (options: BorgeePluginOptions) => PluginClientLike;
|
|
6
6
|
type SdkChatControlPlaneOptions = Pick<BorgeePluginOptions, 'cursorStore' | 'pluginId'>;
|
|
7
7
|
/**
|
|
@@ -34,6 +34,15 @@ export declare class SdkChatControlPlane implements ChatControlPlane {
|
|
|
34
34
|
taskId: string;
|
|
35
35
|
}): Promise<Task>;
|
|
36
36
|
updateTask(input: UpdateTaskInput): Promise<Task>;
|
|
37
|
+
setTaskProperty(input: {
|
|
38
|
+
taskId: string;
|
|
39
|
+
key: string;
|
|
40
|
+
value: string;
|
|
41
|
+
}): Promise<Task>;
|
|
42
|
+
deleteTaskProperty(input: {
|
|
43
|
+
taskId: string;
|
|
44
|
+
key: string;
|
|
45
|
+
}): Promise<Task>;
|
|
37
46
|
}
|
|
38
47
|
export declare function mapInboundToChannelMessage(event: InboundMessageEvent): ChannelMessageEvent;
|
|
39
48
|
export {};
|
|
@@ -123,6 +123,12 @@ export class SdkChatControlPlane {
|
|
|
123
123
|
async updateTask(input) {
|
|
124
124
|
return this.client.updateTask(input);
|
|
125
125
|
}
|
|
126
|
+
async setTaskProperty(input) {
|
|
127
|
+
return this.client.setTaskProperty(input);
|
|
128
|
+
}
|
|
129
|
+
async deleteTaskProperty(input) {
|
|
130
|
+
return this.client.deleteTaskProperty(input);
|
|
131
|
+
}
|
|
126
132
|
}
|
|
127
133
|
export function mapInboundToChannelMessage(event) {
|
|
128
134
|
return {
|
package/dist/cli-args.d.ts
CHANGED
|
@@ -85,4 +85,4 @@ export declare function parseCleanupManagedArgs(argv: string[]): ParsedCleanupMa
|
|
|
85
85
|
export declare function parseApplyManagedArgs(argv: string[]): ParsedApplyManagedArgs;
|
|
86
86
|
/** `update` always installs the latest published release; it takes no options. */
|
|
87
87
|
export declare function assertNoUpdateArgs(argv: string[]): void;
|
|
88
|
-
export declare const USAGE = "Usage:\n agents-host start <serverUrl> <apiKey> [options]\n agents-host start-managed <serverUrl>\n agents-host start-managed <serverUrl> <apiKey> [agent-options]\n agents-host describe-managed <serverUrl>\n agents-host cleanup-managed <serverUrl> [--purge]\n agents-host apply-managed <serverUrl> --stdin\n agents-host apply-managed <serverUrl> --spec-json <json>\n agents-host log <serverUrl> [--lines <n>] [--follow]\n agents-host log <serverUrl> --path\n agents-host start --config <path-to-host-config> [--debug]\n agents-host validate --config <path-to-host-config>\n agents-host describe --config <path-to-host-config>\n agents-host print-layout --root <dir>\n agents-host generate-config --root <dir> --stdin\n agents-host generate-config --root <dir> --spec-json <json>\n agents-host update\n\nForeground start options:\n --debug Enable host/provider debug logs (or set AGENTS_HOST_DEBUG=1)\n\nSingle-agent agent options:\n --name <name> Display name (default: Assistant)\n --provider <claude|codex|copilot>\n Runtime provider (default: claude)\n --claude-command <cmd> Local Claude ACP adapter command (default: claude-agent-acp)\n --claude-args <args> Local Claude ACP adapter args (empty on the shipped bundled path)\n --codex-command <cmd> Local Codex ACP adapter command (default: codex-acp)\n --codex-args <args> Local Codex ACP adapter args\n --copilot-command <cmd> Local Copilot CLI command (default: copilot)\n --copilot-args <args> Ignored by the Copilot ACP prototype\n --copilot-session-ttl-minutes <minutes>\n Idle session TTL for Copilot ACP sessions (default: 2880)\n\nCommand summary:\n start <serverUrl> <apiKey> Start one foreground hosted agent (legacy-compatible behavior)\n start --config <path> Start agents + supervisor + watchers from a host config file\n start-managed <serverUrl> Restart or resume an existing per-serverUrl managed daemon\n start-managed <serverUrl> <apiKey>\n Upsert one agent into the per-serverUrl local daemon and exit after reconcile\n describe-managed <serverUrl> Print managed-runtime spec JSON for machine callers\n cleanup-managed <serverUrl> Stop one managed-runtime daemon locally; --purge also removes its runtime root\n apply-managed <serverUrl> Apply one managed-runtime full-set spec for machine callers\n log <serverUrl> Print the managed daemon log tail for one server runtime\n validate --config <path> Validate local-config files without starting agents or watchers\n describe --config <path> Print the current managed full-set spec as JSON\n print-layout --root <dir> Print the canonical default local-config layout as JSON\n generate-config --root <dir> --stdin Materialize canonical local-config files from stdin\n generate-config --root <dir> --spec-json <json>\n Compatibility input; JSON is exposed in process arguments\n update Update the globally installed @borgee/agents-host to the latest release\n\nStartup update check:\n Startup commands print an advisory notice on stderr when a newer release is published.\n Set AGENTS_HOST_DISABLE_UPDATE_CHECK=1 to turn that check off.\n\nExamples:\n agents-host start https://borgee.example.com bgr_xxxxxxxx --provider copilot --debug\n agents-host start-managed https://borgee.example.com\n agents-host start-managed https://borgee.example.com bgr_xxxxxxxx --provider copilot\n agents-host describe-managed https://borgee.example.com\n agents-host cleanup-managed https://borgee.example.com --purge\n printf '%s' '{\"host\":{\"borgeeBaseUrl\":\"https://borgee.example.com\"},\"agents\":[{\"key\":\"cp1\",\"name\":\"Copilot\",\"apiKey\":\"bgr_xxx\",\"provider\":\"copilot\"}]}' | agents-host apply-managed https://borgee.example.com --stdin\n agents-host log https://borgee.example.com --lines 200 --follow\n agents-host log https://borgee.example.com --path\n agents-host start --config ./agents-host.yaml --debug\n agents-host validate --config ./agents-host.yaml\n agents-host describe --config ./agents-host.yaml\n agents-host print-layout --root ./runtime-root\n printf '%s' '{\"host\":{\"borgeeBaseUrl\":\"https://borgee.example.com\"},\"agents\":[{\"key\":\"cp1\",\"name\":\"Copilot\",\"apiKey\":\"bgr_xxx\",\"provider\":\"copilot\"}]}' | agents-host generate-config --root ./runtime-root --stdin\n agents-host update\n";
|
|
88
|
+
export declare const USAGE = "Usage:\n agents-host start <serverUrl> <apiKey> [options]\n agents-host start-managed <serverUrl>\n agents-host start-managed <serverUrl> <apiKey> [agent-options]\n agents-host describe-managed <serverUrl>\n agents-host cleanup-managed <serverUrl> [--purge]\n agents-host apply-managed <serverUrl> --stdin\n agents-host apply-managed <serverUrl> --spec-json <json>\n agents-host log <serverUrl> [--lines <n>] [--follow]\n agents-host log <serverUrl> --path\n agents-host start --config <path-to-host-config> [--debug]\n agents-host validate --config <path-to-host-config>\n agents-host describe --config <path-to-host-config>\n agents-host print-layout --root <dir>\n agents-host generate-config --root <dir> --stdin\n agents-host generate-config --root <dir> --spec-json <json>\n agents-host update\n\nForeground start options:\n --debug Enable host/provider debug logs (or set AGENTS_HOST_DEBUG=1)\n\nSingle-agent agent options:\n --name <name> Display name (default: Assistant)\n --provider <claude|codex|copilot>\n Runtime provider (default: claude)\n --claude-command <cmd> Local Claude ACP adapter command (default: claude-agent-acp)\n --claude-args <args> Local Claude ACP adapter args (empty on the shipped bundled path)\n --codex-command <cmd> Local Codex ACP adapter command (default: codex-acp)\n --codex-args <args> Local Codex ACP adapter args\n --copilot-command <cmd> Local Copilot CLI command (default: copilot)\n --copilot-args <args> Ignored by the Copilot ACP prototype\n --copilot-session-ttl-minutes <minutes>\n Idle session TTL for Copilot ACP sessions (default: 2880)\n --provider-idle-shutdown-minutes <minutes>\n Shut the shared ACP adapter process down after this much idle\n time and re-spawn it on the next turn; 0 keeps it resident\n (default: 10)\n\nCommand summary:\n start <serverUrl> <apiKey> Start one foreground hosted agent (legacy-compatible behavior)\n start --config <path> Start agents + supervisor + watchers from a host config file\n start-managed <serverUrl> Restart or resume an existing per-serverUrl managed daemon\n start-managed <serverUrl> <apiKey>\n Upsert one agent into the per-serverUrl local daemon and exit after reconcile\n describe-managed <serverUrl> Print managed-runtime spec JSON for machine callers\n cleanup-managed <serverUrl> Stop one managed-runtime daemon locally; --purge also removes its runtime root\n apply-managed <serverUrl> Apply one managed-runtime full-set spec for machine callers\n log <serverUrl> Print the managed daemon log tail for one server runtime\n validate --config <path> Validate local-config files without starting agents or watchers\n describe --config <path> Print the current managed full-set spec as JSON\n print-layout --root <dir> Print the canonical default local-config layout as JSON\n generate-config --root <dir> --stdin Materialize canonical local-config files from stdin\n generate-config --root <dir> --spec-json <json>\n Compatibility input; JSON is exposed in process arguments\n update Update the globally installed @borgee/agents-host to the latest release\n\nStartup update check:\n Startup commands print an advisory notice on stderr when a newer release is published.\n Set AGENTS_HOST_DISABLE_UPDATE_CHECK=1 to turn that check off.\n\nExamples:\n agents-host start https://borgee.example.com bgr_xxxxxxxx --provider copilot --debug\n agents-host start-managed https://borgee.example.com\n agents-host start-managed https://borgee.example.com bgr_xxxxxxxx --provider copilot\n agents-host describe-managed https://borgee.example.com\n agents-host cleanup-managed https://borgee.example.com --purge\n printf '%s' '{\"host\":{\"borgeeBaseUrl\":\"https://borgee.example.com\"},\"agents\":[{\"key\":\"cp1\",\"name\":\"Copilot\",\"apiKey\":\"bgr_xxx\",\"provider\":\"copilot\"}]}' | agents-host apply-managed https://borgee.example.com --stdin\n agents-host log https://borgee.example.com --lines 200 --follow\n agents-host log https://borgee.example.com --path\n agents-host start --config ./agents-host.yaml --debug\n agents-host validate --config ./agents-host.yaml\n agents-host describe --config ./agents-host.yaml\n agents-host print-layout --root ./runtime-root\n printf '%s' '{\"host\":{\"borgeeBaseUrl\":\"https://borgee.example.com\"},\"agents\":[{\"key\":\"cp1\",\"name\":\"Copilot\",\"apiKey\":\"bgr_xxx\",\"provider\":\"copilot\"}]}' | agents-host generate-config --root ./runtime-root --stdin\n agents-host update\n";
|
package/dist/cli-args.js
CHANGED
|
@@ -15,6 +15,7 @@ export const CLI_FLAG_TO_ENV = {
|
|
|
15
15
|
'copilot-command': 'COPILOT_COMMAND',
|
|
16
16
|
'copilot-args': 'COPILOT_ARGS',
|
|
17
17
|
'copilot-session-ttl-minutes': 'COPILOT_SESSION_TTL_MINUTES',
|
|
18
|
+
'provider-idle-shutdown-minutes': 'PROVIDER_IDLE_SHUTDOWN_MINUTES',
|
|
18
19
|
};
|
|
19
20
|
const SINGLE_AGENT_FLAG_NAMES = new Set(Object.keys(CLI_FLAG_TO_ENV));
|
|
20
21
|
const FLAGS_ALLOWING_DASH_PREFIX_VALUE = new Set(['claude-args', 'codex-args', 'copilot-args']);
|
|
@@ -512,6 +513,10 @@ Single-agent agent options:
|
|
|
512
513
|
--copilot-args <args> Ignored by the Copilot ACP prototype
|
|
513
514
|
--copilot-session-ttl-minutes <minutes>
|
|
514
515
|
Idle session TTL for Copilot ACP sessions (default: 2880)
|
|
516
|
+
--provider-idle-shutdown-minutes <minutes>
|
|
517
|
+
Shut the shared ACP adapter process down after this much idle
|
|
518
|
+
time and re-spawn it on the next turn; 0 keeps it resident
|
|
519
|
+
(default: 10)
|
|
515
520
|
|
|
516
521
|
Command summary:
|
|
517
522
|
start <serverUrl> <apiKey> Start one foreground hosted agent (legacy-compatible behavior)
|
|
@@ -13,6 +13,7 @@ export declare const COLLABORATION_CAPABILITIES_DIAGNOSTICS_COMPATIBILITY_GATE =
|
|
|
13
13
|
export declare const TOKEN_BINDING_COMPATIBILITY_GATE = "token-binding";
|
|
14
14
|
export declare const POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE = "policy-audit-enforcement";
|
|
15
15
|
export declare const MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE = "managed-runtime-convergence";
|
|
16
|
+
export declare const PROVIDER_IDLE_SHUTDOWN_COMPATIBILITY_GATE = "provider-idle-shutdown";
|
|
16
17
|
export declare const COMPATIBILITY_GATES_ENV = "AGENTS_HOST_INTERNAL_COMPATIBILITY_GATES";
|
|
17
18
|
export declare const INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV = "AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES";
|
|
18
19
|
export declare const INTERNAL_POLICY_MODE_ENV = "AGENTS_HOST_INTERNAL_POLICY_MODE";
|
|
@@ -14,6 +14,7 @@ export const COLLABORATION_CAPABILITIES_DIAGNOSTICS_COMPATIBILITY_GATE = 'collab
|
|
|
14
14
|
export const TOKEN_BINDING_COMPATIBILITY_GATE = 'token-binding';
|
|
15
15
|
export const POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE = 'policy-audit-enforcement';
|
|
16
16
|
export const MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE = 'managed-runtime-convergence';
|
|
17
|
+
export const PROVIDER_IDLE_SHUTDOWN_COMPATIBILITY_GATE = 'provider-idle-shutdown';
|
|
17
18
|
export const COMPATIBILITY_GATES_ENV = 'AGENTS_HOST_INTERNAL_COMPATIBILITY_GATES';
|
|
18
19
|
export const INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV = 'AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES';
|
|
19
20
|
export const INTERNAL_POLICY_MODE_ENV = 'AGENTS_HOST_INTERNAL_POLICY_MODE';
|
|
@@ -30,6 +31,7 @@ export const DEFAULT_INTERNAL_COMPATIBILITY_GATES = Object.freeze([
|
|
|
30
31
|
LOCALHOST_GATEWAY_COMPATIBILITY_GATE,
|
|
31
32
|
MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE,
|
|
32
33
|
POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE,
|
|
34
|
+
PROVIDER_IDLE_SHUTDOWN_COMPATIBILITY_GATE,
|
|
33
35
|
SKILL_RUNTIME_COMPATIBILITY_GATE,
|
|
34
36
|
TASK_THREAD_COLLABORATION_CONTRACT_COMPATIBILITY_GATE,
|
|
35
37
|
TOKEN_BINDING_COMPATIBILITY_GATE,
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AgentsHostConfig, ProviderCommandConfig, ProviderKind } from './types.js';
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const MAX_TIMER_DELAY_MINUTES: number;
|
|
3
3
|
export declare const DEFAULT_COPILOT_SESSION_TTL_MINUTES: number;
|
|
4
|
+
export declare const PROVIDER_IDLE_SHUTDOWN_DISABLED_MINUTES = 0;
|
|
5
|
+
export declare const DEFAULT_PROVIDER_IDLE_SHUTDOWN_MINUTES = 10;
|
|
4
6
|
export declare const DEFAULT_CLAUDE_COMMAND = "claude-agent-acp";
|
|
5
7
|
export declare const LEGACY_CLAUDE_COMMAND = "claude";
|
|
6
8
|
export declare const LEGACY_CLAUDE_ONE_SHOT_ARGS: readonly ["--print", "--permission-mode", "bypassPermissions"];
|
|
@@ -10,6 +12,7 @@ export declare function optionalNonEmptyString(value: unknown, fieldName: string
|
|
|
10
12
|
export declare function parseArgs(value: string): string[];
|
|
11
13
|
export declare function optionalStringArray(value: unknown, fieldName: string, sourceLabel: string): string[] | undefined;
|
|
12
14
|
export declare function parseCopilotSessionTtlMinutesValue(value: unknown, sourceLabel: string): number;
|
|
15
|
+
export declare function parseProviderIdleShutdownMinutesValue(value: unknown, sourceLabel: string): number;
|
|
13
16
|
export declare function resolveProvider(rawValue: string, sourceLabel: string): ProviderKind;
|
|
14
17
|
export declare function assertProviderCompatibility(provider: ProviderKind, sourceLabel: string, env?: NodeJS.ProcessEnv): void;
|
|
15
18
|
export declare function isLegacyClaudeCompatibilityAlias(command: string, args: string[]): boolean;
|
package/dist/config.js
CHANGED
|
@@ -2,8 +2,10 @@ import { basename } from 'node:path';
|
|
|
2
2
|
import { resolveSingleAgentStateRoot } from './state-paths.js';
|
|
3
3
|
import { CODEX_PROVIDER_COMPATIBILITY_GATE, resolveInternalCompatibilityGates, } from './compatibility-gates.js';
|
|
4
4
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
5
|
-
export const
|
|
5
|
+
export const MAX_TIMER_DELAY_MINUTES = MAX_TIMER_DELAY_MS / 60_000;
|
|
6
6
|
export const DEFAULT_COPILOT_SESSION_TTL_MINUTES = 2 * 24 * 60;
|
|
7
|
+
export const PROVIDER_IDLE_SHUTDOWN_DISABLED_MINUTES = 0;
|
|
8
|
+
export const DEFAULT_PROVIDER_IDLE_SHUTDOWN_MINUTES = 10;
|
|
7
9
|
export const DEFAULT_CLAUDE_COMMAND = 'claude-agent-acp';
|
|
8
10
|
export const LEGACY_CLAUDE_COMMAND = 'claude';
|
|
9
11
|
export const LEGACY_CLAUDE_ONE_SHOT_ARGS = ['--print', '--permission-mode', 'bypassPermissions'];
|
|
@@ -15,6 +17,7 @@ export const DEFAULT_PROVIDER_COMMAND_CONFIG = {
|
|
|
15
17
|
copilotCommand: 'copilot',
|
|
16
18
|
copilotArgs: ['-s', '--no-color', '--allow-all-tools', '--output-format', 'text'],
|
|
17
19
|
copilotSessionTtlMinutes: DEFAULT_COPILOT_SESSION_TTL_MINUTES,
|
|
20
|
+
providerIdleShutdownMinutes: DEFAULT_PROVIDER_IDLE_SHUTDOWN_MINUTES,
|
|
18
21
|
};
|
|
19
22
|
function requireEnv(name, env) {
|
|
20
23
|
return requireNonEmptyString(env[name], `Missing required environment variable: ${name}`);
|
|
@@ -57,8 +60,18 @@ export function parseCopilotSessionTtlMinutesValue(value, sourceLabel) {
|
|
|
57
60
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
58
61
|
throw new Error(`Invalid COPILOT_SESSION_TTL_MINUTES in ${sourceLabel}: expected a positive number`);
|
|
59
62
|
}
|
|
60
|
-
if (parsed >
|
|
61
|
-
throw new Error(`Invalid COPILOT_SESSION_TTL_MINUTES in ${sourceLabel}: must be <= ${
|
|
63
|
+
if (parsed > MAX_TIMER_DELAY_MINUTES) {
|
|
64
|
+
throw new Error(`Invalid COPILOT_SESSION_TTL_MINUTES in ${sourceLabel}: must be <= ${MAX_TIMER_DELAY_MINUTES.toFixed(2)} minutes to fit within the Node.js timer limit`);
|
|
65
|
+
}
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
export function parseProviderIdleShutdownMinutesValue(value, sourceLabel) {
|
|
69
|
+
const parsed = typeof value === 'number' ? value : Number(String(value).trim());
|
|
70
|
+
if (!Number.isFinite(parsed) || parsed < PROVIDER_IDLE_SHUTDOWN_DISABLED_MINUTES) {
|
|
71
|
+
throw new Error(`Invalid PROVIDER_IDLE_SHUTDOWN_MINUTES in ${sourceLabel}: expected a non-negative number, where ${PROVIDER_IDLE_SHUTDOWN_DISABLED_MINUTES} keeps the shared adapter process resident`);
|
|
72
|
+
}
|
|
73
|
+
if (parsed > MAX_TIMER_DELAY_MINUTES) {
|
|
74
|
+
throw new Error(`Invalid PROVIDER_IDLE_SHUTDOWN_MINUTES in ${sourceLabel}: must be <= ${MAX_TIMER_DELAY_MINUTES.toFixed(2)} minutes to fit within the Node.js timer limit`);
|
|
62
75
|
}
|
|
63
76
|
return parsed;
|
|
64
77
|
}
|
|
@@ -119,6 +132,8 @@ export function resolveProviderCommandConfig(overrides = {}) {
|
|
|
119
132
|
copilotCommand: overrides.copilotCommand ?? DEFAULT_PROVIDER_COMMAND_CONFIG.copilotCommand,
|
|
120
133
|
copilotArgs: [...(overrides.copilotArgs ?? DEFAULT_PROVIDER_COMMAND_CONFIG.copilotArgs)],
|
|
121
134
|
copilotSessionTtlMinutes: overrides.copilotSessionTtlMinutes ?? DEFAULT_PROVIDER_COMMAND_CONFIG.copilotSessionTtlMinutes,
|
|
135
|
+
providerIdleShutdownMinutes: overrides.providerIdleShutdownMinutes
|
|
136
|
+
?? DEFAULT_PROVIDER_COMMAND_CONFIG.providerIdleShutdownMinutes,
|
|
122
137
|
};
|
|
123
138
|
}
|
|
124
139
|
export function loadConfigFromEnv(env = process.env) {
|
|
@@ -135,6 +150,9 @@ export function loadConfigFromEnv(env = process.env) {
|
|
|
135
150
|
copilotSessionTtlMinutes: env.COPILOT_SESSION_TTL_MINUTES && env.COPILOT_SESSION_TTL_MINUTES.trim().length > 0
|
|
136
151
|
? parseCopilotSessionTtlMinutesValue(env.COPILOT_SESSION_TTL_MINUTES, 'environment variable COPILOT_SESSION_TTL_MINUTES')
|
|
137
152
|
: DEFAULT_PROVIDER_COMMAND_CONFIG.copilotSessionTtlMinutes,
|
|
153
|
+
providerIdleShutdownMinutes: env.PROVIDER_IDLE_SHUTDOWN_MINUTES && env.PROVIDER_IDLE_SHUTDOWN_MINUTES.trim().length > 0
|
|
154
|
+
? parseProviderIdleShutdownMinutesValue(env.PROVIDER_IDLE_SHUTDOWN_MINUTES, 'environment variable PROVIDER_IDLE_SHUTDOWN_MINUTES')
|
|
155
|
+
: DEFAULT_PROVIDER_COMMAND_CONFIG.providerIdleShutdownMinutes,
|
|
138
156
|
});
|
|
139
157
|
assertProviderCommandCompatibility(provider, providerConfig, 'environment variables');
|
|
140
158
|
return {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MAIN_SESSION_DELEGATION_LINES: readonly string[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const MAIN_SESSION_DELEGATION_LINES = [
|
|
2
|
+
'This session is your coordination surface: read the channel, answer the human, break work down, and keep task state current here.',
|
|
3
|
+
'Delegate the actual work to subagents — research, code edits, long builds and test runs, and broad repository sweeps belong in a delegated worker instead of running inline here.',
|
|
4
|
+
'Delegation changes who executes, not who owns: the work still belongs to this thread, and you still report the result here yourself.',
|
|
5
|
+
'Stay responsive while delegated work runs, so a new incoming message never waits behind a long local operation.',
|
|
6
|
+
];
|
package/dist/context/prompt.js
CHANGED
|
@@ -2,6 +2,7 @@ import { AWAITING_USER_CONTROL_PREFIX } from '../providers/awaiting-user.js';
|
|
|
2
2
|
import { buildAttentionSummaryLines } from './attention.js';
|
|
3
3
|
import { buildCollaborationCapabilityDeclarationSummaryLines, buildMissedCollaborationDiagnosticSummaryLines, } from './collaboration-capabilities-diagnostics.js';
|
|
4
4
|
import { buildCollaborationOutcomeSummaryLines } from './collaboration-outcome.js';
|
|
5
|
+
import { MAIN_SESSION_DELEGATION_LINES } from './main-session-delegation.js';
|
|
5
6
|
import { buildTaskThreadCollaborationSummaryLines } from './task-thread-collaboration.js';
|
|
6
7
|
function providerLabel(provider) {
|
|
7
8
|
if (provider === 'copilot') {
|
|
@@ -45,10 +46,10 @@ function buildLocalhostGatewayPromptLines(context) {
|
|
|
45
46
|
}
|
|
46
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`);
|
|
47
48
|
if (isTaskAssignmentThread) {
|
|
48
|
-
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>"]`);
|
|
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.');
|
|
49
50
|
}
|
|
50
51
|
else {
|
|
51
|
-
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>"]`);
|
|
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.');
|
|
52
53
|
}
|
|
53
54
|
if (context.localhostGateway.collaboration?.enabled &&
|
|
54
55
|
(context.collaborationTurnMode ?? 'ordinary') === 'ordinary') {
|
|
@@ -221,6 +222,7 @@ export function buildPrompt(params) {
|
|
|
221
222
|
'Be concise, helpful, and honest about uncertainty.',
|
|
222
223
|
'Do not claim to have performed actions you did not actually perform.',
|
|
223
224
|
'Keep the visible reply text concise.',
|
|
225
|
+
...MAIN_SESSION_DELEGATION_LINES,
|
|
224
226
|
...buildTurnControlPromptLines(params.promptContext),
|
|
225
227
|
`If the user asks who you are, what powers you, or which backend/provider you use, mention that you are currently running on ${providerLabel(params.provider)}.`,
|
|
226
228
|
`Channel: ${params.channelId}`,
|
|
@@ -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,6 +629,31 @@ 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) {
|
|
616
658
|
let task;
|
|
617
659
|
const preferredTaskId = binding.payload?.taskAssignmentContext?.currentTaskId;
|
|
@@ -816,6 +858,17 @@ function parseUpdateTaskInput(taskId, body) {
|
|
|
816
858
|
}
|
|
817
859
|
return input;
|
|
818
860
|
}
|
|
861
|
+
/**
|
|
862
|
+
* The value is required and must be a string. An absent or non-string value is
|
|
863
|
+
* a caller bug, not an instruction to store the empty string — storing that
|
|
864
|
+
* would silently blank a key the caller meant to set.
|
|
865
|
+
*/
|
|
866
|
+
function parseTaskPropertyValue(body) {
|
|
867
|
+
if (!isRecord(body) || typeof body.value !== 'string') {
|
|
868
|
+
throw new GatewayHttpError(400, { error: 'invalid_property_value' }, 'bad-request');
|
|
869
|
+
}
|
|
870
|
+
return body.value;
|
|
871
|
+
}
|
|
819
872
|
function isTaskInAuthorizedScope(boundChannelId, task) {
|
|
820
873
|
return task.channelId === boundChannelId || task.threadId === boundChannelId;
|
|
821
874
|
}
|
|
@@ -846,6 +899,19 @@ function mapGatewayControlPlaneError(error, fallbackReason) {
|
|
|
846
899
|
case 'bpp.channel_id_required':
|
|
847
900
|
case 'bpp.task_id_required':
|
|
848
901
|
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.
|
|
905
|
+
case 'bpp.task_property_key_unknown':
|
|
906
|
+
return new GatewayHttpError(400, { error: 'unknown_property_key' }, 'bad-request');
|
|
907
|
+
case 'bpp.task_property_value_too_long':
|
|
908
|
+
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.
|
|
913
|
+
case 'bpp.task_property_not_found':
|
|
914
|
+
return new GatewayHttpError(404, { error: 'property_not_found' }, 'not-found');
|
|
849
915
|
default:
|
|
850
916
|
return new GatewayHttpError(502, { error: 'upstream_error' }, fallbackReason);
|
|
851
917
|
}
|
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 = {}) {
|