@borgee/agents-host 0.2.74 → 0.2.94
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 +11 -24
- package/dist/agents-host.d.ts +6 -4
- package/dist/agents-host.js +127 -16
- package/dist/background-runs.d.ts +24 -0
- package/dist/background-runs.js +184 -0
- package/dist/chat/chat-control-plane.d.ts +7 -1
- package/dist/chat/sdk-chat-control-plane.d.ts +7 -2
- package/dist/chat/sdk-chat-control-plane.js +22 -0
- package/dist/context/claude-file-brief.js +3 -3
- package/dist/context/injection.d.ts +18 -12
- package/dist/context/injection.js +53 -53
- package/dist/context/prompt.js +10 -10
- package/dist/context/resolved-working-folder.d.ts +3 -0
- package/dist/context/resolved-working-folder.js +106 -0
- package/dist/context/turn-preparation.js +1 -1
- package/dist/managed-daemon.js +30 -30
- package/dist/plugin-sdk.js +315 -29
- package/dist/plugin-sdk.js.map +3 -3
- package/dist/policy/authorization-audit.d.ts +1 -1
- package/dist/policy/copilot-permission.d.ts +9 -0
- package/dist/policy/copilot-permission.js +120 -1
- package/dist/progress-to-activity.d.ts +1 -1
- package/dist/progress-to-activity.js +1 -0
- package/dist/providers/claude/adapter.d.ts +2 -1
- package/dist/providers/claude/adapter.js +36 -1
- package/dist/providers/claude/background-run-observer.d.ts +32 -0
- package/dist/providers/claude/background-run-observer.js +381 -0
- package/dist/providers/claude/cli-client.d.ts +12 -0
- package/dist/providers/claude/cli-client.js +453 -72
- package/dist/providers/claude/foreground-handoff.d.ts +4 -0
- package/dist/providers/claude/foreground-handoff.js +45 -0
- package/dist/providers/claude/task-cancellation-protocol.d.ts +14 -0
- package/dist/providers/claude/task-cancellation-protocol.js +21 -0
- package/dist/providers/codex/adapter.d.ts +1 -1
- package/dist/providers/codex/adapter.js +7 -0
- package/dist/providers/codex/cli-client.js +8 -7
- package/dist/providers/codex/project-doc.js +6 -6
- package/dist/providers/copilot/activity-metadata.d.ts +3 -0
- package/dist/providers/copilot/activity-metadata.js +19 -0
- package/dist/providers/copilot/adapter.d.ts +6 -2
- package/dist/providers/copilot/adapter.js +27 -1
- package/dist/providers/copilot/cli-client.d.ts +37 -10
- package/dist/providers/copilot/cli-client.js +756 -122
- package/dist/providers/copilot/sdk-session.d.ts +149 -0
- package/dist/providers/copilot/sdk-session.js +981 -0
- package/dist/providers/create-provider.js +33 -0
- package/dist/providers/provider-adapter.d.ts +24 -1
- package/dist/providers/provider-adapter.js +17 -0
- package/dist/types.d.ts +40 -11
- package/dist/vendor/claude-agent-acp/LICENSE +191 -0
- package/dist/vendor/claude-agent-acp/NOTICE +8 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.d.ts +1017 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.js +6305 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-foreground-handoff-bridge.js +322 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-task-cancellation-bridge.js +101 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-task-lifecycle-bridge.js +58 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.d.ts +130 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.js +304 -0
- package/dist/vendor/claude-agent-acp/dist/index.d.ts +3 -0
- package/dist/vendor/claude-agent-acp/dist/index.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/index.js +75 -0
- package/dist/vendor/claude-agent-acp/dist/lib.d.ts +6 -0
- package/dist/vendor/claude-agent-acp/dist/lib.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/lib.js +5 -0
- package/dist/vendor/claude-agent-acp/dist/settings.d.ts +68 -0
- package/dist/vendor/claude-agent-acp/dist/settings.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/settings.js +185 -0
- package/dist/vendor/claude-agent-acp/dist/tools.d.ts +102 -0
- package/dist/vendor/claude-agent-acp/dist/tools.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/tools.js +1000 -0
- package/dist/vendor/claude-agent-acp/dist/utils.d.ts +16 -0
- package/dist/vendor/claude-agent-acp/dist/utils.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/utils.js +81 -0
- package/dist/vendor/claude-agent-acp/package.json +85 -0
- package/package.json +15 -10
- package/skills/borgee-agent/references/task-properties.md +3 -3
- package/skills/borgee-agent/scripts/borgee-agent.mjs +1 -1
- package/skills/borgee-agent/scripts/borgee-agent.py +1 -1
- package/dist/context/resolved-workspace.d.ts +0 -3
- package/dist/context/resolved-workspace.js +0 -106
package/README.md
CHANGED
|
@@ -101,7 +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
|
|
104
|
+
- `AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES=provider-idle-shutdown` keeps every provider runtime process resident again; `PROVIDER_IDLE_SHUTDOWN_MINUTES=0` does the same without touching gates
|
|
105
105
|
|
|
106
106
|
Or with the CLI:
|
|
107
107
|
|
|
@@ -239,11 +239,11 @@ Notes:
|
|
|
239
239
|
| `RUNTIME_PROVIDER` | `--provider` | no | `claude` | `claude`, `copilot`, or `codex` |
|
|
240
240
|
| `CLAUDE_COMMAND` / `CLAUDE_ARGS` | `--claude-command` / `--claude-args` | no | `claude-agent-acp` / empty | Local Claude ACP adapter command + args. The shipped bundled `claude-agent-acp` path requires empty `CLAUDE_ARGS`; non-empty args are only valid when `CLAUDE_COMMAND` points at a custom ACP adapter command. Legacy `claude --print --permission-mode bypassPermissions` is tolerated as a compatibility alias. |
|
|
241
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. |
|
|
242
|
-
| `COPILOT_COMMAND` / `COPILOT_ARGS` | `--copilot-command` / `--copilot-args` | no | `copilot` / parsed but ignored
|
|
243
|
-
| `COPILOT_SESSION_TTL_MINUTES` | `--copilot-session-ttl-minutes` | no | `2880` | Idle TTL for per-channel Copilot
|
|
244
|
-
| `PROVIDER_IDLE_SHUTDOWN_MINUTES` | `--provider-idle-shutdown-minutes` | no | `10` | Idle window before the agent's shared
|
|
242
|
+
| `COPILOT_COMMAND` / `COPILOT_ARGS` | `--copilot-command` / `--copilot-args` | no | `copilot` / parsed but ignored | Local Copilot CLI command. The host resolves it to an executable and starts the SDK headless JSON-RPC runtime; legacy `COPILOT_ARGS` remain ignored. |
|
|
243
|
+
| `COPILOT_SESSION_TTL_MINUTES` | `--copilot-session-ttl-minutes` | no | `2880` | Idle TTL for per-channel Copilot SDK sessions |
|
|
244
|
+
| `PROVIDER_IDLE_SHUTDOWN_MINUTES` | `--provider-idle-shutdown-minutes` | no | `10` | Idle window before the agent's shared provider runtime **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
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
|
|
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 SDK *session* inside a running runtime process. The second shuts the shared provider *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.
|
|
247
247
|
|
|
248
248
|
## Local-config mode
|
|
249
249
|
|
|
@@ -479,14 +479,14 @@ successfully applied snapshot running.
|
|
|
479
479
|
Each Borgee channel is mapped 1:1 to a provider-native session while it stays
|
|
480
480
|
active:
|
|
481
481
|
|
|
482
|
-
- **Claude**: one persistent `@agentclientprotocol/claude-agent-acp` subprocess is shared by a single `AgentsHost`, with one ACP session per Borgee channel. Same-channel turns are serialized; different channels keep isolated ACP sessions. The real ACP session cwd now comes from the resolved channel
|
|
482
|
+
- **Claude**: one persistent `@agentclientprotocol/claude-agent-acp` subprocess is shared by a single `AgentsHost`, with one ACP session per Borgee channel. Same-channel turns are serialized; different channels keep isolated ACP sessions. The real ACP session cwd now comes from the resolved channel working folder `~/.borgee/channels/<encoded-channel-id>/workspace`. Task threads only switch execution onto an explicit existing `execution.local_directory`; otherwise they stay discussion-only on `~/.borgee/channels/<encoded-thread-id>/scratch/workspace`, and ACP permission requests are denied so filesystem and shell tools stay unavailable. The host persists the channel→ACP-session map in its state root, restores same-host continuity with ACP `session/resume` when available, retries stale restores with a fresh session immediately, and injects the per-channel local context paths through Claude ACP `_meta.systemPrompt.append`. The bundled adapter advertises a versioned targeted task-stop extension backed by the Agent SDK's `Query.stopTask(taskId)`: accepting that request moves only the selected background run to `stopping`, while its provider lifecycle event decides the terminal state and the parent turn plus sibling workers continue unchanged. Custom ACP commands do not receive this capability and remain fail-closed.
|
|
483
483
|
- **Codex**: one persistent `@agentclientprotocol/codex-acp` subprocess is
|
|
484
484
|
shared by a single `AgentsHost`, with one ACP session per Borgee channel. When
|
|
485
485
|
the current turn has a materialized channel context payload, Codex refreshes a
|
|
486
486
|
same-directory `AGENTS.md` project document inside a provider-visible
|
|
487
487
|
per-channel projection directory and exposes that directory to Codex through
|
|
488
488
|
ACP `additionalDirectories`. The real ACP session cwd now comes from the
|
|
489
|
-
resolved channel
|
|
489
|
+
resolved channel working folder `~/.borgee/channels/<encoded-channel-id>/workspace`.
|
|
490
490
|
Task threads only switch execution onto an explicit existing
|
|
491
491
|
`execution.local_directory`; otherwise they stay discussion-only on
|
|
492
492
|
`~/.borgee/channels/<encoded-thread-id>/scratch/workspace`, and ACP
|
|
@@ -494,35 +494,22 @@ active:
|
|
|
494
494
|
unavailable. The host persists the
|
|
495
495
|
channel→ACP-session map in its state root and attempts ACP `session/resume`
|
|
496
496
|
first when the adapter advertises it, otherwise `session/load`.
|
|
497
|
-
- **Copilot**: one persistent
|
|
498
|
-
`AgentsHost`, with one ACP session per Borgee channel. Same-channel turns are
|
|
499
|
-
serialized; different channels keep isolated ACP sessions. The real ACP
|
|
500
|
-
session cwd now comes from the resolved channel workspace
|
|
501
|
-
`~/.borgee/channels/<encoded-channel-id>/workspace` by default. Task threads
|
|
502
|
-
only switch execution onto an explicit existing `execution.local_directory`;
|
|
503
|
-
otherwise they stay discussion-only on
|
|
504
|
-
`~/.borgee/channels/<encoded-thread-id>/scratch/workspace`, and ACP
|
|
505
|
-
permission requests are denied so filesystem and shell tools stay
|
|
506
|
-
unavailable. The
|
|
507
|
-
host persists the channel→ACP-session map in its state root and restores
|
|
508
|
-
same-host continuity with ACP `session/resume` when available, otherwise
|
|
509
|
-
`session/load` plus local replay cleanup on runtimes that advertise only
|
|
510
|
-
load support.
|
|
497
|
+
- **Copilot**: one persistent SDK-managed headless Copilot subprocess is shared by a single `AgentsHost`, with one SDK session per Borgee channel. Same-channel turns are serialized; different channels keep isolated sessions. The real session cwd comes from the resolved channel working folder `~/.borgee/channels/<encoded-channel-id>/workspace` by default. Task threads only switch execution onto an explicit existing `execution.local_directory`; otherwise they stay discussion-only on `~/.borgee/channels/<encoded-thread-id>/scratch/workspace`, and SDK permission requests are denied so filesystem and shell tools stay unavailable. The host persists the channel→session map in its state root and restores same-host continuity through SDK session resume. A replacement human prompt uses the SDK's foreground-only interrupt and keeps background work alive; explicit stop interrupts the foreground, cancels background agents, and cancels active background shell tasks in that session.
|
|
511
498
|
|
|
512
499
|
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.
|
|
513
500
|
|
|
514
501
|
### Idle adapter shutdown
|
|
515
502
|
|
|
516
|
-
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
|
|
503
|
+
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 provider 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.
|
|
517
504
|
|
|
518
|
-
Conversation memory normally survives the recycle. Before the process goes away the host closes each live
|
|
505
|
+
Conversation memory normally survives the recycle. Before the process goes away the host closes each live provider 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 SDK session resume. This is the same restore path a host restart takes, and its ordinary outcome is that the model still remembers the conversation.
|
|
519
506
|
|
|
520
507
|
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:
|
|
521
508
|
|
|
522
509
|
- **A different machine or a different `$HOME`.** The session store is local, so continuity never crosses hosts.
|
|
523
510
|
- **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.
|
|
524
511
|
- **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)).
|
|
525
|
-
- **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 if the resolved channel
|
|
512
|
+
- **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 if the resolved channel working folder path changes, or if the injected provider-visible directory set changes, the host deliberately drops the old mapping and starts a fresh session even though the CLI could still load it.
|
|
526
513
|
|
|
527
514
|
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`.
|
|
528
515
|
|
package/dist/agents-host.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class AgentsHost {
|
|
|
11
11
|
private readonly runtime;
|
|
12
12
|
private readonly provider;
|
|
13
13
|
private readonly borgee;
|
|
14
|
+
private readonly backgroundRuns;
|
|
14
15
|
private readonly logger;
|
|
15
16
|
private readonly contextInjectionEnabled;
|
|
16
17
|
private readonly collaborationEnabled;
|
|
@@ -33,6 +34,7 @@ export declare class AgentsHost {
|
|
|
33
34
|
private readonly compactionSnapshotWriteQueueByChannel;
|
|
34
35
|
private readonly missedCollaborationDiagnosticByChannel;
|
|
35
36
|
private readonly channelTurnQueues;
|
|
37
|
+
private readonly autonomousReplyKeys;
|
|
36
38
|
private readonly collaborationDrafts;
|
|
37
39
|
private readonly collaborationChannels;
|
|
38
40
|
private readonly participantDirectoryByUserId;
|
|
@@ -54,6 +56,7 @@ export declare class AgentsHost {
|
|
|
54
56
|
stop(): Promise<void>;
|
|
55
57
|
private trackActiveTurn;
|
|
56
58
|
private dispatchMessage;
|
|
59
|
+
private dispatchAutonomousReply;
|
|
57
60
|
private handleLegacyMessage;
|
|
58
61
|
private handleCollaborationMessage;
|
|
59
62
|
private detectProtocolKickoffCandidate;
|
|
@@ -144,10 +147,9 @@ export declare class AgentsHost {
|
|
|
144
147
|
* exists — including its ending, which a window opened before it was replaced
|
|
145
148
|
* would otherwise carry out on its behalf.
|
|
146
149
|
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* same answer for a turn that keeps its reply to itself.
|
|
150
|
+
* Silent internal turns do not open a reporter. Deferred turns still report
|
|
151
|
+
* because they represent user-visible work even when final delivery happens
|
|
152
|
+
* elsewhere.
|
|
151
153
|
*/
|
|
152
154
|
private beginTurnActivity;
|
|
153
155
|
private canAcceptTurnOutput;
|
package/dist/agents-host.js
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
4
4
|
import { SdkChatControlPlane } from './chat/sdk-chat-control-plane.js';
|
|
5
5
|
import { HostLogger, summarizeError } from './debug.js';
|
|
6
6
|
import { toReportedActivity } from './progress-to-activity.js';
|
|
7
|
+
import { BackgroundRunTracker } from './background-runs.js';
|
|
7
8
|
import { ATTENTION_FOLLOW_SEMANTICS_COMPATIBILITY_GATE, COLLABORATION_CAPABILITIES_DIAGNOSTICS_COMPATIBILITY_GATE, COLLABORATION_OUTCOME_MODEL_COMPATIBILITY_GATE, COLLABORATION_SKILL_FIRST_COMPATIBILITY_GATE, CONTEXT_INJECTION_COMPATIBILITY_GATE, LOCALHOST_GATEWAY_COMPATIBILITY_GATE, POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE, resolveInternalPolicyMode, resolveInternalCompatibilityGates, SKILL_RUNTIME_COMPATIBILITY_GATE, TASK_THREAD_COLLABORATION_CONTRACT_COMPATIBILITY_GATE, TOKEN_BINDING_COMPATIBILITY_GATE, } from './compatibility-gates.js';
|
|
8
9
|
import { buildNormalizedAttentionSnapshot, parsePersistedAttentionSnapshot, } from './context/attention.js';
|
|
9
10
|
import { mergeCompactionSnapshot, parsePersistedCompactionSnapshot, } from './context/compaction.js';
|
|
@@ -149,15 +150,19 @@ class DefaultHostRuntime {
|
|
|
149
150
|
});
|
|
150
151
|
this.ensureStateRoot = deps.ensureStateRoot ?? ensurePrivateStateRoot;
|
|
151
152
|
}
|
|
152
|
-
async start(onMessage, onStopTurn) {
|
|
153
|
+
async start(onMessage, onStopTurn, onStopBackgroundRun, onActivityTransportReady) {
|
|
153
154
|
await this.ensureStateRoot(this.config.stateRootDir);
|
|
154
155
|
await this.gateway.start();
|
|
155
156
|
try {
|
|
156
157
|
this.controlPlane.onStopTurn?.(onStopTurn);
|
|
158
|
+
this.controlPlane.onStopBackgroundRun?.(onStopBackgroundRun);
|
|
159
|
+
this.controlPlane.onActivityTransportReady?.(onActivityTransportReady);
|
|
157
160
|
await this.controlPlane.connect(onMessage);
|
|
158
161
|
}
|
|
159
162
|
catch (error) {
|
|
160
163
|
this.controlPlane.onStopTurn?.(undefined);
|
|
164
|
+
this.controlPlane.onStopBackgroundRun?.(undefined);
|
|
165
|
+
this.controlPlane.onActivityTransportReady?.(undefined);
|
|
161
166
|
await this.gateway.stop().catch(() => { });
|
|
162
167
|
throw error;
|
|
163
168
|
}
|
|
@@ -166,6 +171,8 @@ class DefaultHostRuntime {
|
|
|
166
171
|
let thrown;
|
|
167
172
|
try {
|
|
168
173
|
this.controlPlane.onStopTurn?.(undefined);
|
|
174
|
+
this.controlPlane.onStopBackgroundRun?.(undefined);
|
|
175
|
+
this.controlPlane.onActivityTransportReady?.(undefined);
|
|
169
176
|
await this.controlPlane.close();
|
|
170
177
|
}
|
|
171
178
|
catch (error) {
|
|
@@ -272,6 +279,7 @@ const AGENT_SESSION_TASK_PROPERTY_KEY = 'agent.session_id';
|
|
|
272
279
|
const BLOCKED_AUTHOR_REFRESH_BACKOFF_MS = 1_000;
|
|
273
280
|
const MAX_QUEUED_MESSAGES_PER_CHANNEL = 8;
|
|
274
281
|
const COLLABORATION_SENDS_PER_TURN = 1;
|
|
282
|
+
const MAX_AUTONOMOUS_REPLY_DEDUPLICATION_KEYS = 256;
|
|
275
283
|
/**
|
|
276
284
|
* Minimal single-agent agents host: connects one local Claude/Copilot CLI
|
|
277
285
|
* to exactly one Borgee agent over `@borgee/plugin-sdk` (BPP / `/ws/plugin`).
|
|
@@ -281,6 +289,7 @@ export class AgentsHost {
|
|
|
281
289
|
runtime;
|
|
282
290
|
provider;
|
|
283
291
|
borgee;
|
|
292
|
+
backgroundRuns;
|
|
284
293
|
logger;
|
|
285
294
|
contextInjectionEnabled;
|
|
286
295
|
collaborationEnabled;
|
|
@@ -303,6 +312,7 @@ export class AgentsHost {
|
|
|
303
312
|
compactionSnapshotWriteQueueByChannel = new Map();
|
|
304
313
|
missedCollaborationDiagnosticByChannel = new Map();
|
|
305
314
|
channelTurnQueues = new Map();
|
|
315
|
+
autonomousReplyKeys = new Map();
|
|
306
316
|
collaborationDrafts = new Map();
|
|
307
317
|
collaborationChannels = new Map();
|
|
308
318
|
participantDirectoryByUserId = new Map();
|
|
@@ -349,6 +359,16 @@ export class AgentsHost {
|
|
|
349
359
|
});
|
|
350
360
|
this.provider = this.runtime.provider;
|
|
351
361
|
this.borgee = this.runtime.controlPlane;
|
|
362
|
+
const backgroundRunCapabilities = this.provider.capabilities?.backgroundRuns;
|
|
363
|
+
this.backgroundRuns = new BackgroundRunTracker((channelId, activity) => {
|
|
364
|
+
this.borgee.reportActivity?.({ channelId, activity });
|
|
365
|
+
}, {
|
|
366
|
+
lifecycleSupported: backgroundRunCapabilities?.lifecycle === 'supported',
|
|
367
|
+
targetedCancellationSupported: backgroundRunCapabilities?.targetedCancellation === 'supported',
|
|
368
|
+
cancel: this.provider.cancelBackgroundRun
|
|
369
|
+
? (channelId, providerRunId) => (this.provider.cancelBackgroundRun(channelId, providerRunId))
|
|
370
|
+
: undefined,
|
|
371
|
+
});
|
|
352
372
|
}
|
|
353
373
|
async start() {
|
|
354
374
|
if (this.started)
|
|
@@ -364,6 +384,7 @@ export class AgentsHost {
|
|
|
364
384
|
const bufferedMessages = [];
|
|
365
385
|
let bufferingMessages = true;
|
|
366
386
|
try {
|
|
387
|
+
this.provider.onAutonomousReply?.((reply) => this.dispatchAutonomousReply(reply));
|
|
367
388
|
await this.runtime.start((message) => {
|
|
368
389
|
if (!this.started) {
|
|
369
390
|
return;
|
|
@@ -373,7 +394,7 @@ export class AgentsHost {
|
|
|
373
394
|
return;
|
|
374
395
|
}
|
|
375
396
|
this.dispatchMessage(message);
|
|
376
|
-
}, async (channelId) => this.stopTurn(channelId));
|
|
397
|
+
}, async (channelId) => this.stopTurn(channelId), async (channelId, runId) => this.backgroundRuns.stop(channelId, runId), () => this.backgroundRuns.replay());
|
|
377
398
|
const agentId = await this.ensureSelfAgentId();
|
|
378
399
|
bufferingMessages = false;
|
|
379
400
|
while (bufferedMessages.length > 0) {
|
|
@@ -393,6 +414,7 @@ export class AgentsHost {
|
|
|
393
414
|
bufferedMessages.length = 0;
|
|
394
415
|
this.started = false;
|
|
395
416
|
this.controlPlaneClosed = true;
|
|
417
|
+
this.provider.onAutonomousReply?.(undefined);
|
|
396
418
|
await this.runtime.stop().catch(() => { });
|
|
397
419
|
throw error;
|
|
398
420
|
}
|
|
@@ -401,6 +423,7 @@ export class AgentsHost {
|
|
|
401
423
|
if (!this.started)
|
|
402
424
|
return;
|
|
403
425
|
this.started = false;
|
|
426
|
+
this.provider.onAutonomousReply?.(undefined);
|
|
404
427
|
this.logger.debug('stopping host', {
|
|
405
428
|
activeTurnCount: this.activeTurns.size,
|
|
406
429
|
provider: this.config.agent.provider,
|
|
@@ -420,6 +443,7 @@ export class AgentsHost {
|
|
|
420
443
|
this.activeCompactionByChannel.clear();
|
|
421
444
|
this.compactionSnapshotLoadedChannels.clear();
|
|
422
445
|
this.missedCollaborationDiagnosticByChannel.clear();
|
|
446
|
+
this.autonomousReplyKeys.clear();
|
|
423
447
|
this.recordedAgentSessionByTask.clear();
|
|
424
448
|
this.controlPlaneClosed = true;
|
|
425
449
|
const pendingCompactionSnapshotWrites = [...this.compactionSnapshotWriteQueueByChannel.values()];
|
|
@@ -449,6 +473,57 @@ export class AgentsHost {
|
|
|
449
473
|
: this.enqueueChannelTurn(message.channel_id, () => this.handleLegacyMessage(message));
|
|
450
474
|
this.trackActiveTurn(task);
|
|
451
475
|
}
|
|
476
|
+
dispatchAutonomousReply(reply) {
|
|
477
|
+
if (!this.started
|
|
478
|
+
|| this.controlPlaneClosed
|
|
479
|
+
|| !reply.channelId.trim()
|
|
480
|
+
|| !reply.eventId.trim()
|
|
481
|
+
|| !hasVisibleText(reply.text)) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const key = `${reply.channelId}\0${reply.eventId}`;
|
|
485
|
+
if (this.autonomousReplyKeys.has(key)) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
this.autonomousReplyKeys.set(key, true);
|
|
489
|
+
while (this.autonomousReplyKeys.size > MAX_AUTONOMOUS_REPLY_DEDUPLICATION_KEYS) {
|
|
490
|
+
const oldest = this.autonomousReplyKeys.keys().next().value;
|
|
491
|
+
if (typeof oldest !== 'string') {
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
this.autonomousReplyKeys.delete(oldest);
|
|
495
|
+
}
|
|
496
|
+
const task = this.enqueueChannelTurn(reply.channelId, async () => {
|
|
497
|
+
if (!this.started || this.controlPlaneClosed) {
|
|
498
|
+
this.autonomousReplyKeys.delete(key);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
await this.borgee.postMessage({
|
|
503
|
+
channelId: reply.channelId,
|
|
504
|
+
body: reply.text,
|
|
505
|
+
});
|
|
506
|
+
this.logger.debug('posted provider autonomous reply', {
|
|
507
|
+
channelId: reply.channelId,
|
|
508
|
+
provider: this.config.agent.provider,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
this.autonomousReplyKeys.delete(key);
|
|
513
|
+
this.logger.debugError('failed to post provider autonomous reply', {
|
|
514
|
+
channelId: reply.channelId,
|
|
515
|
+
provider: this.config.agent.provider,
|
|
516
|
+
error,
|
|
517
|
+
});
|
|
518
|
+
this.logger.error('failed to post provider autonomous reply', {
|
|
519
|
+
channelId: reply.channelId,
|
|
520
|
+
provider: this.config.agent.provider,
|
|
521
|
+
error: summarizeError(error),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
this.trackActiveTurn(task);
|
|
526
|
+
}
|
|
452
527
|
async handleLegacyMessage(msg) {
|
|
453
528
|
const normalized = await this.normalizeMessage(msg);
|
|
454
529
|
if (!normalized) {
|
|
@@ -570,8 +645,20 @@ export class AgentsHost {
|
|
|
570
645
|
if (this.isHumanWakeFiltered(msg, authorKind, attentionSnapshot)) {
|
|
571
646
|
return;
|
|
572
647
|
}
|
|
648
|
+
let foregroundTransition;
|
|
573
649
|
if (authorKind !== 'agent' && channelState.activeTurn) {
|
|
574
650
|
this.supersedeActiveTurn(msg.channel_id, channelState.activeTurn);
|
|
651
|
+
foregroundTransition = (async () => {
|
|
652
|
+
try {
|
|
653
|
+
await this.provider.interruptTurn?.(msg.channel_id);
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
this.logger.error('failed to interrupt superseded provider turn; replacement remains queued', {
|
|
657
|
+
channelId: msg.channel_id,
|
|
658
|
+
error: summarizeError(error),
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
})();
|
|
575
662
|
}
|
|
576
663
|
if (authorKind !== 'agent') {
|
|
577
664
|
channelState.recentTurn = undefined;
|
|
@@ -585,6 +672,7 @@ export class AgentsHost {
|
|
|
585
672
|
attentionSnapshot,
|
|
586
673
|
taskThreadContext,
|
|
587
674
|
});
|
|
675
|
+
await foregroundTransition;
|
|
588
676
|
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
589
677
|
}
|
|
590
678
|
detectProtocolKickoffCandidate(anchorMessageId, content, selfAgentId) {
|
|
@@ -969,6 +1057,9 @@ export class AgentsHost {
|
|
|
969
1057
|
async ensureCollaborationProcessor(channelId, channelState) {
|
|
970
1058
|
if (channelState.processor) {
|
|
971
1059
|
await channelState.processor;
|
|
1060
|
+
if (channelState.queue.length > 0 && !channelState.processor) {
|
|
1061
|
+
await this.ensureCollaborationProcessor(channelId, channelState);
|
|
1062
|
+
}
|
|
972
1063
|
return;
|
|
973
1064
|
}
|
|
974
1065
|
const processor = this.processCollaborationQueue(channelId, channelState).finally(() => {
|
|
@@ -2230,7 +2321,10 @@ export class AgentsHost {
|
|
|
2230
2321
|
this.collaborationEnabled &&
|
|
2231
2322
|
turnExecutionId != null &&
|
|
2232
2323
|
activeTurn?.protocol === undefined;
|
|
2233
|
-
const
|
|
2324
|
+
const supportsProviderProgress = this.provider.capabilities?.progressUpdates?.support === 'supported';
|
|
2325
|
+
const supportsBackgroundRunLifecycle = this.provider.capabilities?.backgroundRuns?.lifecycle === 'supported';
|
|
2326
|
+
const reportsProviderActivity = !silentTurn && supportsProviderProgress;
|
|
2327
|
+
const observeProviderProgress = supportsProviderProgress || supportsBackgroundRunLifecycle;
|
|
2234
2328
|
const turnContext = {
|
|
2235
2329
|
eventType: msg.type,
|
|
2236
2330
|
channelId,
|
|
@@ -2257,7 +2351,9 @@ export class AgentsHost {
|
|
|
2257
2351
|
text: String(msg.content ?? msg.body ?? content),
|
|
2258
2352
|
attachments: msg.attachments,
|
|
2259
2353
|
});
|
|
2260
|
-
const activityReporter =
|
|
2354
|
+
const activityReporter = silentTurn
|
|
2355
|
+
? undefined
|
|
2356
|
+
: this.beginTurnActivity(channelId, activeTurn?.generation);
|
|
2261
2357
|
await this.ensureCompactionSnapshotLoaded(channelId);
|
|
2262
2358
|
const compactionSnapshot = this.compactionSnapshotByChannel.get(channelId);
|
|
2263
2359
|
try {
|
|
@@ -2304,20 +2400,32 @@ export class AgentsHost {
|
|
|
2304
2400
|
}, observeProviderProgress
|
|
2305
2401
|
? {
|
|
2306
2402
|
onProgress: (update) => {
|
|
2403
|
+
if (supportsBackgroundRunLifecycle) {
|
|
2404
|
+
this.backgroundRuns.observe(channelId, update);
|
|
2405
|
+
if (update.type === 'activity' && update.activity.background === true) {
|
|
2406
|
+
this.borgee.reportActivity?.({
|
|
2407
|
+
channelId,
|
|
2408
|
+
activity: { shape: 'activity', ...update.activity },
|
|
2409
|
+
});
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2307
2413
|
if (!this.canAcceptTurnOutput(channelId, activeTurn?.generation)) {
|
|
2308
2414
|
return;
|
|
2309
2415
|
}
|
|
2310
|
-
if (update.type === 'compaction') {
|
|
2416
|
+
if (supportsProviderProgress && update.type === 'compaction') {
|
|
2311
2417
|
this.noteCompactionProgress(channelId, update.compaction, {
|
|
2312
2418
|
turnExecutionId: activeTurn?.turnExecutionId ?? undefined,
|
|
2313
2419
|
});
|
|
2314
2420
|
return;
|
|
2315
2421
|
}
|
|
2316
2422
|
const reportedActivity = toReportedActivity(update);
|
|
2317
|
-
if (reportedActivity) {
|
|
2318
|
-
activityReporter
|
|
2423
|
+
if (reportsProviderActivity && reportedActivity) {
|
|
2424
|
+
activityReporter?.progress(reportedActivity);
|
|
2319
2425
|
}
|
|
2320
|
-
if (
|
|
2426
|
+
if (!supportsProviderProgress
|
|
2427
|
+
|| update.type !== 'text'
|
|
2428
|
+
|| update.stream !== 'answer') {
|
|
2321
2429
|
return;
|
|
2322
2430
|
}
|
|
2323
2431
|
if (!publishesCollaborationDraft) {
|
|
@@ -2527,7 +2635,7 @@ export class AgentsHost {
|
|
|
2527
2635
|
}
|
|
2528
2636
|
finally {
|
|
2529
2637
|
stopTyping();
|
|
2530
|
-
activityReporter
|
|
2638
|
+
activityReporter?.end();
|
|
2531
2639
|
}
|
|
2532
2640
|
}
|
|
2533
2641
|
/**
|
|
@@ -2539,10 +2647,9 @@ export class AgentsHost {
|
|
|
2539
2647
|
* exists — including its ending, which a window opened before it was replaced
|
|
2540
2648
|
* would otherwise carry out on its behalf.
|
|
2541
2649
|
*
|
|
2542
|
-
*
|
|
2543
|
-
*
|
|
2544
|
-
*
|
|
2545
|
-
* same answer for a turn that keeps its reply to itself.
|
|
2650
|
+
* Silent internal turns do not open a reporter. Deferred turns still report
|
|
2651
|
+
* because they represent user-visible work even when final delivery happens
|
|
2652
|
+
* elsewhere.
|
|
2546
2653
|
*/
|
|
2547
2654
|
beginTurnActivity(channelId, generation) {
|
|
2548
2655
|
const reporter = this.borgee.reportTurnActivity({
|
|
@@ -2710,8 +2817,11 @@ export class AgentsHost {
|
|
|
2710
2817
|
turnExecutionId: activeTurn.turnExecutionId,
|
|
2711
2818
|
});
|
|
2712
2819
|
}
|
|
2820
|
+
// Output acceptance checks `superseded`, so a stale foreground turn still
|
|
2821
|
+
// cannot stream, draft, or post a visible reply after the interrupt. The
|
|
2822
|
+
// id stays on the running turn until it unwinds so background children that
|
|
2823
|
+
// inherited it can finish their own gateway work cleanly.
|
|
2713
2824
|
activeTurn.superseded = true;
|
|
2714
|
-
activeTurn.turnExecutionId = null;
|
|
2715
2825
|
}
|
|
2716
2826
|
invalidateChannelTurn(channelId) {
|
|
2717
2827
|
const activeTurn = this.collaborationChannels.get(channelId)?.activeTurn;
|
|
@@ -2721,7 +2831,9 @@ export class AgentsHost {
|
|
|
2721
2831
|
}
|
|
2722
2832
|
async stopTurn(channelId) {
|
|
2723
2833
|
const activeTurn = this.collaborationChannels.get(channelId)?.activeTurn;
|
|
2724
|
-
const aborted = await this.provider.
|
|
2834
|
+
const aborted = await (this.provider.cancelSession?.(channelId)
|
|
2835
|
+
?? this.provider.cancelTurn?.(channelId)
|
|
2836
|
+
?? false);
|
|
2725
2837
|
if (!aborted) {
|
|
2726
2838
|
return { aborted: false };
|
|
2727
2839
|
}
|
|
@@ -2759,7 +2871,6 @@ export class AgentsHost {
|
|
|
2759
2871
|
resolveAuthorizedCollaborationTurn(channelState, turnExecutionId) {
|
|
2760
2872
|
const activeTurn = channelState?.activeTurn;
|
|
2761
2873
|
if (activeTurn &&
|
|
2762
|
-
!activeTurn.superseded &&
|
|
2763
2874
|
activeTurn.turnExecutionId != null &&
|
|
2764
2875
|
activeTurn.turnExecutionId === turnExecutionId) {
|
|
2765
2876
|
return activeTurn;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentActivity } from './plugin-sdk.js';
|
|
2
|
+
import type { ProviderProgressUpdate } from './types.js';
|
|
3
|
+
interface BackgroundRunTrackerOptions {
|
|
4
|
+
lifecycleSupported: boolean;
|
|
5
|
+
targetedCancellationSupported: boolean;
|
|
6
|
+
cancel?: (channelId: string, providerRunId: string) => Promise<boolean>;
|
|
7
|
+
}
|
|
8
|
+
export declare class BackgroundRunTracker {
|
|
9
|
+
private readonly report;
|
|
10
|
+
private readonly options;
|
|
11
|
+
private readonly now;
|
|
12
|
+
private readonly mintRunId;
|
|
13
|
+
private readonly runs;
|
|
14
|
+
private readonly runsById;
|
|
15
|
+
constructor(report: (channelId: string, activity: AgentActivity) => void, options: BackgroundRunTrackerOptions, now?: () => number, mintRunId?: (channelId: string, providerRunId: string, startedAt: number, executionId?: string) => string);
|
|
16
|
+
observe(channelId: string, update: ProviderProgressUpdate): void;
|
|
17
|
+
replay(): void;
|
|
18
|
+
stop(channelId: string, runId: string): Promise<{
|
|
19
|
+
outcome: 'stopped' | 'not_found' | 'unsupported';
|
|
20
|
+
reason?: string;
|
|
21
|
+
}>;
|
|
22
|
+
private publish;
|
|
23
|
+
}
|
|
24
|
+
export {};
|