@borgee/agents-host 0.2.33 → 0.2.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -7
- package/dist/agents-host.d.ts +19 -0
- package/dist/agents-host.js +163 -44
- package/dist/chat/chat-control-plane.d.ts +9 -0
- package/dist/chat/sdk-chat-control-plane.d.ts +10 -1
- package/dist/chat/sdk-chat-control-plane.js +9 -0
- package/dist/cli-args.d.ts +1 -1
- package/dist/cli-args.js +5 -0
- package/dist/compatibility-gates.d.ts +1 -0
- package/dist/compatibility-gates.js +2 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +21 -3
- package/dist/context/injection.d.ts +5 -3
- package/dist/context/injection.js +45 -29
- package/dist/context/main-session-delegation.d.ts +1 -0
- package/dist/context/main-session-delegation.js +6 -0
- package/dist/context/prompt.js +25 -35
- package/dist/context/skill-manual.d.ts +13 -0
- package/dist/context/skill-manual.js +18 -0
- package/dist/context/turn-preparation.js +13 -4
- package/dist/gateway/localhost-gateway.js +75 -1
- package/dist/hosted-turn-content.d.ts +15 -0
- package/dist/hosted-turn-content.js +50 -0
- package/dist/local-config.js +10 -1
- package/dist/managed-daemon.d.ts +3 -2
- package/dist/managed-daemon.js +82 -29
- package/dist/plugin-sdk.js +58 -1
- package/dist/plugin-sdk.js.map +2 -2
- package/dist/policy/gateway-authorization.d.ts +18 -3
- package/dist/policy/gateway-authorization.js +33 -1
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +13 -1
- package/dist/providers/claude/cli-client.d.ts +36 -3
- package/dist/providers/claude/cli-client.js +225 -37
- package/dist/providers/codex/adapter.d.ts +3 -1
- package/dist/providers/codex/adapter.js +13 -1
- package/dist/providers/codex/cli-client.d.ts +35 -3
- package/dist/providers/codex/cli-client.js +212 -31
- package/dist/providers/codex/project-doc.js +16 -29
- package/dist/providers/copilot/adapter.d.ts +3 -1
- package/dist/providers/copilot/adapter.js +13 -1
- package/dist/providers/copilot/cli-client.d.ts +34 -2
- package/dist/providers/copilot/cli-client.js +196 -18
- package/dist/providers/create-provider.d.ts +1 -1
- package/dist/providers/create-provider.js +30 -14
- package/dist/providers/idle-backend-shutdown.d.ts +16 -0
- package/dist/providers/idle-backend-shutdown.js +53 -0
- package/dist/providers/provider-adapter.d.ts +35 -0
- package/dist/providers/provider-adapter.js +44 -1
- package/dist/state-paths.d.ts +9 -1
- package/dist/state-paths.js +22 -3
- package/dist/types.d.ts +40 -2
- package/package.json +2 -2
- package/skills/borgee-agent/SKILL.md +133 -35
- package/skills/borgee-agent/references/errors.md +38 -0
- package/skills/borgee-agent/references/task-properties.md +30 -0
- package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
- package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
- package/skills/borgee-agent/borgee-agent.mjs +0 -507
- package/skills/borgee-agent/borgee-agent.py +0 -438
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;
|
|
@@ -88,11 +93,25 @@ export declare class AgentsHost {
|
|
|
88
93
|
private armProtocolFallback;
|
|
89
94
|
private runProtocolFallback;
|
|
90
95
|
private handleProtocolFallbackFailure;
|
|
96
|
+
private ensureProtocolAnchorIncomingParts;
|
|
91
97
|
private finishProtocolState;
|
|
92
98
|
private cancelProtocolState;
|
|
93
99
|
private confirmProtocolMessage;
|
|
100
|
+
private findChannelHistoryMessageById;
|
|
94
101
|
private resolveAttentionStatePath;
|
|
95
102
|
private buildDefaultAttentionSnapshot;
|
|
103
|
+
/**
|
|
104
|
+
* Record which provider session worked this task, as agent.session_id.
|
|
105
|
+
*
|
|
106
|
+
* agents-host writes it rather than the agent: the session id is runtime
|
|
107
|
+
* state the agent has no reliable view of, so asking it to report its own
|
|
108
|
+
* would make the association depend on the agent remembering to.
|
|
109
|
+
*
|
|
110
|
+
* Best-effort and off the turn's critical path — a task whose session id
|
|
111
|
+
* failed to record is still a task that ran, so a failure here must neither
|
|
112
|
+
* fail the turn nor delay it.
|
|
113
|
+
*/
|
|
114
|
+
private recordAgentSessionOnTask;
|
|
96
115
|
private buildTurnTaskThreadContext;
|
|
97
116
|
private ensureAttentionSnapshotLoaded;
|
|
98
117
|
private revalidateAttentionSnapshotForTurn;
|
package/dist/agents-host.js
CHANGED
|
@@ -14,6 +14,7 @@ import { createProvider } from './providers/create-provider.js';
|
|
|
14
14
|
import { resolveAttentionStatePath, resolveSharedProtocolKickoffDecisionPath, resolveSharedProtocolStatusPath, } from './state-paths.js';
|
|
15
15
|
import { appendVisibleMention as appendVisibleBodyMention, extractVisibleMentionIds as extractCanonicalMentionIds, } from './visible-mentions.js';
|
|
16
16
|
import { waitForTaskForThread } from './task-thread-resolution.js';
|
|
17
|
+
import { buildHostedIncomingContentText, buildHostedTurnContentParts, extractHostedMessageAttachments, normalizeHostedMessageAttachment, } from './hosted-turn-content.js';
|
|
17
18
|
const PRIVATE_STATE_ROOT_MODE = 0o700;
|
|
18
19
|
const STREAM_PROGRESS_EDIT_THROTTLE_MS = 150;
|
|
19
20
|
const PROTOCOL_STATE_DIRNAME = 'protocol-state';
|
|
@@ -126,7 +127,9 @@ class DefaultHostRuntime {
|
|
|
126
127
|
deps.provider ??
|
|
127
128
|
createProvider({
|
|
128
129
|
provider: config.agent.provider,
|
|
130
|
+
borgeeBaseUrl: config.borgeeBaseUrl,
|
|
129
131
|
stateRootDir: config.stateRootDir,
|
|
132
|
+
agentApiKey: config.agent.agentApiKey,
|
|
130
133
|
resolveStableAgentId: deps.resolveStableAgentId,
|
|
131
134
|
claudeCommand: config.claudeCommand,
|
|
132
135
|
claudeArgs: config.claudeArgs,
|
|
@@ -135,6 +138,7 @@ class DefaultHostRuntime {
|
|
|
135
138
|
copilotCommand: config.copilotCommand,
|
|
136
139
|
copilotArgs: config.copilotArgs,
|
|
137
140
|
copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
|
|
141
|
+
providerIdleShutdownMinutes: config.providerIdleShutdownMinutes,
|
|
138
142
|
}, deps.logger, {
|
|
139
143
|
compatibilityGates,
|
|
140
144
|
authorizationAuditSink: this.authorizationAuditSink,
|
|
@@ -361,6 +365,11 @@ class DraftMessageController {
|
|
|
361
365
|
return this.writeChain;
|
|
362
366
|
}
|
|
363
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* The registered task property agents-host writes to record which provider
|
|
370
|
+
* session worked a task. It matches the server's registry key.
|
|
371
|
+
*/
|
|
372
|
+
const AGENT_SESSION_TASK_PROPERTY_KEY = 'agent.session_id';
|
|
364
373
|
const BLOCKED_AUTHOR_REFRESH_BACKOFF_MS = 1_000;
|
|
365
374
|
const MAX_QUEUED_MESSAGES_PER_CHANNEL = 8;
|
|
366
375
|
const COLLABORATION_SENDS_PER_TURN = 1;
|
|
@@ -383,6 +392,11 @@ export class AgentsHost {
|
|
|
383
392
|
activeTurns = new Set();
|
|
384
393
|
awaitingUserByChannel = new Map();
|
|
385
394
|
collaborationOutcomeByChannel = new Map();
|
|
395
|
+
/**
|
|
396
|
+
* The session id last recorded per task, so a multi-turn task writes the
|
|
397
|
+
* property once rather than on every turn that reuses the same session.
|
|
398
|
+
*/
|
|
399
|
+
recordedAgentSessionByTask = new Map();
|
|
386
400
|
attentionSnapshotByChannel = new Map();
|
|
387
401
|
missedCollaborationDiagnosticByChannel = new Map();
|
|
388
402
|
progressChannelQueues = new Map();
|
|
@@ -504,6 +518,7 @@ export class AgentsHost {
|
|
|
504
518
|
this.collaborationOutcomeByChannel.clear();
|
|
505
519
|
this.attentionSnapshotByChannel.clear();
|
|
506
520
|
this.missedCollaborationDiagnosticByChannel.clear();
|
|
521
|
+
this.recordedAgentSessionByTask.clear();
|
|
507
522
|
this.controlPlaneClosed = true;
|
|
508
523
|
await this.runtime.stop();
|
|
509
524
|
await Promise.allSettled([...this.activeTurns]);
|
|
@@ -547,7 +562,7 @@ export class AgentsHost {
|
|
|
547
562
|
}
|
|
548
563
|
}
|
|
549
564
|
}
|
|
550
|
-
await this.runUngatedTurn(msg, normalized.authorId, normalized.content, taskThreadContext, attentionSnapshot);
|
|
565
|
+
await this.runUngatedTurn(msg, normalized.authorId, normalized.content, taskThreadContext, attentionSnapshot, normalized.incomingParts);
|
|
551
566
|
}
|
|
552
567
|
async handleCollaborationMessage(msg) {
|
|
553
568
|
const normalized = await this.normalizeMessage(msg);
|
|
@@ -596,6 +611,7 @@ export class AgentsHost {
|
|
|
596
611
|
authorId: normalized.authorId,
|
|
597
612
|
authorKind: 'unknown',
|
|
598
613
|
content: normalized.content,
|
|
614
|
+
incomingParts: normalized.incomingParts,
|
|
599
615
|
attentionSnapshot,
|
|
600
616
|
taskThreadContext,
|
|
601
617
|
});
|
|
@@ -614,6 +630,7 @@ export class AgentsHost {
|
|
|
614
630
|
authorId: normalized.authorId,
|
|
615
631
|
authorKind: 'human',
|
|
616
632
|
content: normalized.content,
|
|
633
|
+
incomingParts: normalized.incomingParts,
|
|
617
634
|
attentionSnapshot,
|
|
618
635
|
taskThreadContext,
|
|
619
636
|
wakeBlockedState,
|
|
@@ -646,6 +663,7 @@ export class AgentsHost {
|
|
|
646
663
|
authorId: normalized.authorId,
|
|
647
664
|
authorKind,
|
|
648
665
|
content: normalized.content,
|
|
666
|
+
incomingParts: normalized.incomingParts,
|
|
649
667
|
attentionSnapshot,
|
|
650
668
|
taskThreadContext,
|
|
651
669
|
});
|
|
@@ -740,6 +758,7 @@ export class AgentsHost {
|
|
|
740
758
|
const executed = await this.executeTurn(msg, normalized.authorId, normalized.content, {
|
|
741
759
|
kickoff: protocolCandidate,
|
|
742
760
|
silent: true,
|
|
761
|
+
incomingParts: normalized.incomingParts,
|
|
743
762
|
});
|
|
744
763
|
if (executed.stale) {
|
|
745
764
|
return null;
|
|
@@ -900,6 +919,7 @@ export class AgentsHost {
|
|
|
900
919
|
authorId: normalized.authorId,
|
|
901
920
|
authorKind: 'human',
|
|
902
921
|
content: normalized.content,
|
|
922
|
+
incomingParts: normalized.incomingParts,
|
|
903
923
|
});
|
|
904
924
|
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
905
925
|
return;
|
|
@@ -912,6 +932,7 @@ export class AgentsHost {
|
|
|
912
932
|
ownerAgentId: selfAgentId,
|
|
913
933
|
anchorAuthorId: normalized.authorId,
|
|
914
934
|
anchorContent: normalized.content,
|
|
935
|
+
anchorIncomingParts: normalized.incomingParts,
|
|
915
936
|
anchorMessageId: msg.message_id ?? randomUUID(),
|
|
916
937
|
participantIds: protocolRequest.participantIds,
|
|
917
938
|
issuerId: protocolRequest.issuerId,
|
|
@@ -934,6 +955,7 @@ export class AgentsHost {
|
|
|
934
955
|
authorId: normalized.authorId,
|
|
935
956
|
authorKind: 'human',
|
|
936
957
|
content: normalized.content,
|
|
958
|
+
incomingParts: normalized.incomingParts,
|
|
937
959
|
});
|
|
938
960
|
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
939
961
|
return;
|
|
@@ -1002,6 +1024,7 @@ export class AgentsHost {
|
|
|
1002
1024
|
authorId: normalized.authorId,
|
|
1003
1025
|
authorKind: 'agent',
|
|
1004
1026
|
content: normalized.content,
|
|
1027
|
+
incomingParts: normalized.incomingParts,
|
|
1005
1028
|
});
|
|
1006
1029
|
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
1007
1030
|
}
|
|
@@ -1119,6 +1142,7 @@ export class AgentsHost {
|
|
|
1119
1142
|
channelState.activeTurn = activeTurn;
|
|
1120
1143
|
const executed = await this.executeTurn(message.raw, message.authorId, this.buildIncomingContent(message), {
|
|
1121
1144
|
activeTurn,
|
|
1145
|
+
incomingParts: message.incomingParts,
|
|
1122
1146
|
incomingAuthorKind: message.authorKind,
|
|
1123
1147
|
attentionSnapshot,
|
|
1124
1148
|
taskThreadContext,
|
|
@@ -1222,6 +1246,7 @@ export class AgentsHost {
|
|
|
1222
1246
|
channelState.activeTurn = activeTurn;
|
|
1223
1247
|
const executed = await this.executeTurn(message.raw, message.authorId, this.buildIncomingContent(message), {
|
|
1224
1248
|
activeTurn,
|
|
1249
|
+
incomingParts: message.incomingParts,
|
|
1225
1250
|
incomingAuthorKind: message.authorKind,
|
|
1226
1251
|
attentionSnapshot,
|
|
1227
1252
|
taskThreadContext,
|
|
@@ -1529,59 +1554,52 @@ export class AgentsHost {
|
|
|
1529
1554
|
}
|
|
1530
1555
|
async buildResumedProtocolMessage(channelId, protocolState) {
|
|
1531
1556
|
if (protocolState.currentTurnIndex === 0) {
|
|
1557
|
+
const anchorIncomingParts = await this.ensureProtocolAnchorIncomingParts(channelId, protocolState);
|
|
1558
|
+
const anchorIncomingContent = buildHostedIncomingContentText(anchorIncomingParts);
|
|
1532
1559
|
return {
|
|
1533
1560
|
kind: 'protocol-turn',
|
|
1534
1561
|
raw: {
|
|
1535
1562
|
channel_id: channelId,
|
|
1536
1563
|
message_id: protocolState.anchorMessageId,
|
|
1537
1564
|
user_id: protocolState.anchorAuthorId,
|
|
1538
|
-
content:
|
|
1539
|
-
body:
|
|
1565
|
+
content: anchorIncomingContent,
|
|
1566
|
+
body: anchorIncomingContent,
|
|
1567
|
+
attachments: extractHostedMessageAttachments(anchorIncomingParts),
|
|
1540
1568
|
type: 'message',
|
|
1541
1569
|
},
|
|
1542
1570
|
authorId: protocolState.anchorAuthorId,
|
|
1543
1571
|
authorKind: 'human',
|
|
1544
|
-
content:
|
|
1572
|
+
content: anchorIncomingContent,
|
|
1573
|
+
incomingParts: anchorIncomingParts,
|
|
1545
1574
|
};
|
|
1546
1575
|
}
|
|
1547
1576
|
if (!protocolState.lastProtocolMessageId) {
|
|
1548
1577
|
return null;
|
|
1549
1578
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
const
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
limit: 200,
|
|
1579
|
+
const priorMessage = await this.findChannelHistoryMessageById(channelId, protocolState.lastProtocolMessageId);
|
|
1580
|
+
if (priorMessage) {
|
|
1581
|
+
const incomingParts = buildHostedTurnContentParts({
|
|
1582
|
+
text: priorMessage.body,
|
|
1583
|
+
attachments: priorMessage.attachments,
|
|
1556
1584
|
});
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
}
|
|
1575
|
-
if (history.length < 200) {
|
|
1576
|
-
break;
|
|
1577
|
-
}
|
|
1578
|
-
const oldestCreatedAt = history.reduce((oldest, entry) => {
|
|
1579
|
-
return oldest == null || entry.createdAt < oldest ? entry.createdAt : oldest;
|
|
1580
|
-
}, null);
|
|
1581
|
-
if (oldestCreatedAt == null || (before != null && oldestCreatedAt >= before)) {
|
|
1582
|
-
break;
|
|
1583
|
-
}
|
|
1584
|
-
before = oldestCreatedAt;
|
|
1585
|
+
const incomingContent = buildHostedIncomingContentText(incomingParts);
|
|
1586
|
+
return {
|
|
1587
|
+
kind: 'protocol-turn',
|
|
1588
|
+
raw: {
|
|
1589
|
+
channel_id: channelId,
|
|
1590
|
+
message_id: priorMessage.id,
|
|
1591
|
+
user_id: priorMessage.authorId,
|
|
1592
|
+
content: incomingContent,
|
|
1593
|
+
body: incomingContent,
|
|
1594
|
+
attachments: priorMessage.attachments,
|
|
1595
|
+
type: 'message',
|
|
1596
|
+
created_at: priorMessage.createdAt,
|
|
1597
|
+
},
|
|
1598
|
+
authorId: priorMessage.authorId,
|
|
1599
|
+
authorKind: 'agent',
|
|
1600
|
+
content: incomingContent,
|
|
1601
|
+
incomingParts,
|
|
1602
|
+
};
|
|
1585
1603
|
}
|
|
1586
1604
|
return null;
|
|
1587
1605
|
}
|
|
@@ -1592,6 +1610,7 @@ export class AgentsHost {
|
|
|
1592
1610
|
ownerAgentId,
|
|
1593
1611
|
anchorAuthorId: protocolState.anchorAuthorId,
|
|
1594
1612
|
anchorContent: protocolState.anchorContent,
|
|
1613
|
+
anchorIncomingParts: protocolState.anchorIncomingParts,
|
|
1595
1614
|
anchorMessageId: protocolState.anchorMessageId,
|
|
1596
1615
|
participantIds: protocolState.participantIds,
|
|
1597
1616
|
issuerId: protocolState.issuerId,
|
|
@@ -1670,18 +1689,36 @@ export class AgentsHost {
|
|
|
1670
1689
|
await this.persistProtocolState(channelId, protocolState);
|
|
1671
1690
|
return;
|
|
1672
1691
|
}
|
|
1692
|
+
const anchorIncomingParts = await this.ensureProtocolAnchorIncomingParts(channelId, protocolState);
|
|
1693
|
+
const anchorIncomingContent = buildHostedIncomingContentText(anchorIncomingParts);
|
|
1673
1694
|
const fallbackMessage = {
|
|
1674
1695
|
channel_id: channelId,
|
|
1675
1696
|
message_id: protocolState.anchorMessageId,
|
|
1676
1697
|
user_id: protocolState.anchorAuthorId,
|
|
1677
|
-
content:
|
|
1678
|
-
body:
|
|
1698
|
+
content: anchorIncomingContent,
|
|
1699
|
+
body: anchorIncomingContent,
|
|
1700
|
+
attachments: extractHostedMessageAttachments(anchorIncomingParts),
|
|
1679
1701
|
type: 'message',
|
|
1680
1702
|
};
|
|
1681
1703
|
this.cancelProtocolState(channelId, channelState);
|
|
1682
1704
|
const taskThreadContext = await this.buildTurnTaskThreadContext(fallbackMessage, channelId);
|
|
1683
1705
|
const attentionSnapshot = this.getAttentionSnapshotForTurn(channelId, taskThreadContext);
|
|
1684
|
-
await this.runUngatedTurn(fallbackMessage, protocolState.anchorAuthorId,
|
|
1706
|
+
await this.runUngatedTurn(fallbackMessage, protocolState.anchorAuthorId, anchorIncomingContent, taskThreadContext, attentionSnapshot, anchorIncomingParts);
|
|
1707
|
+
}
|
|
1708
|
+
async ensureProtocolAnchorIncomingParts(channelId, protocolState) {
|
|
1709
|
+
if (protocolState.anchorIncomingParts) {
|
|
1710
|
+
return protocolState.anchorIncomingParts;
|
|
1711
|
+
}
|
|
1712
|
+
const anchorMessage = await this.findChannelHistoryMessageById(channelId, protocolState.anchorMessageId);
|
|
1713
|
+
if (!anchorMessage) {
|
|
1714
|
+
return buildHostedTurnContentParts({ text: protocolState.anchorContent });
|
|
1715
|
+
}
|
|
1716
|
+
const anchorIncomingParts = buildHostedTurnContentParts({
|
|
1717
|
+
text: protocolState.anchorContent,
|
|
1718
|
+
attachments: anchorMessage.attachments,
|
|
1719
|
+
});
|
|
1720
|
+
protocolState.anchorIncomingParts = anchorIncomingParts;
|
|
1721
|
+
return anchorIncomingParts;
|
|
1685
1722
|
}
|
|
1686
1723
|
finishProtocolState(channelId, channelState, protocolState, lastMessageId) {
|
|
1687
1724
|
protocolState.active = false;
|
|
@@ -1721,6 +1758,36 @@ export class AgentsHost {
|
|
|
1721
1758
|
}
|
|
1722
1759
|
return matched;
|
|
1723
1760
|
}
|
|
1761
|
+
async findChannelHistoryMessageById(channelId, messageId) {
|
|
1762
|
+
let before;
|
|
1763
|
+
for (;;) {
|
|
1764
|
+
const history = await this.borgee.readChannelHistory({
|
|
1765
|
+
channelId,
|
|
1766
|
+
before,
|
|
1767
|
+
limit: 200,
|
|
1768
|
+
});
|
|
1769
|
+
const matched = history.find((entry) => entry.id === messageId);
|
|
1770
|
+
if (matched) {
|
|
1771
|
+
return {
|
|
1772
|
+
...matched,
|
|
1773
|
+
...(matched.attachments
|
|
1774
|
+
? { attachments: matched.attachments.map(normalizeHostedMessageAttachment) }
|
|
1775
|
+
: {}),
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
if (history.length < 200) {
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1781
|
+
const oldestCreatedAt = history.reduce((oldest, entry) => {
|
|
1782
|
+
return oldest == null || entry.createdAt < oldest ? entry.createdAt : oldest;
|
|
1783
|
+
}, null);
|
|
1784
|
+
if (oldestCreatedAt == null || (before != null && oldestCreatedAt >= before)) {
|
|
1785
|
+
break;
|
|
1786
|
+
}
|
|
1787
|
+
before = oldestCreatedAt;
|
|
1788
|
+
}
|
|
1789
|
+
return null;
|
|
1790
|
+
}
|
|
1724
1791
|
resolveAttentionStatePath(channelId) {
|
|
1725
1792
|
return resolveAttentionStatePath(this.config.stateRootDir, channelId);
|
|
1726
1793
|
}
|
|
@@ -1730,6 +1797,41 @@ export class AgentsHost {
|
|
|
1730
1797
|
turnExecutionId: options?.turnExecutionId,
|
|
1731
1798
|
});
|
|
1732
1799
|
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Record which provider session worked this task, as agent.session_id.
|
|
1802
|
+
*
|
|
1803
|
+
* agents-host writes it rather than the agent: the session id is runtime
|
|
1804
|
+
* state the agent has no reliable view of, so asking it to report its own
|
|
1805
|
+
* would make the association depend on the agent remembering to.
|
|
1806
|
+
*
|
|
1807
|
+
* Best-effort and off the turn's critical path — a task whose session id
|
|
1808
|
+
* failed to record is still a task that ran, so a failure here must neither
|
|
1809
|
+
* fail the turn nor delay it.
|
|
1810
|
+
*/
|
|
1811
|
+
async recordAgentSessionOnTask(channelId, taskThreadContext, sessionId) {
|
|
1812
|
+
const taskId = taskThreadContext?.state === 'active' ? taskThreadContext.currentTaskId : undefined;
|
|
1813
|
+
if (!taskId || !sessionId) {
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
if (this.recordedAgentSessionByTask.get(taskId) === sessionId) {
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
try {
|
|
1820
|
+
await this.borgee.setTaskProperty({
|
|
1821
|
+
taskId,
|
|
1822
|
+
key: AGENT_SESSION_TASK_PROPERTY_KEY,
|
|
1823
|
+
value: sessionId,
|
|
1824
|
+
});
|
|
1825
|
+
this.recordedAgentSessionByTask.set(taskId, sessionId);
|
|
1826
|
+
}
|
|
1827
|
+
catch (error) {
|
|
1828
|
+
this.logger.debugError('best-effort agent session property write failed', {
|
|
1829
|
+
channelId,
|
|
1830
|
+
taskId,
|
|
1831
|
+
error,
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1733
1835
|
async buildTurnTaskThreadContext(msg, channelId) {
|
|
1734
1836
|
if (msg.message_type === 'task_assignment') {
|
|
1735
1837
|
const currentTaskId = extractTaskIdFromTaskAssignmentContent(String(msg.content ?? msg.body ?? ''));
|
|
@@ -1990,10 +2092,11 @@ export class AgentsHost {
|
|
|
1990
2092
|
reason: 'wake-suppressed-attention-policy',
|
|
1991
2093
|
});
|
|
1992
2094
|
}
|
|
1993
|
-
async runUngatedTurn(msg, authorId, content, taskThreadContext, attentionSnapshot) {
|
|
2095
|
+
async runUngatedTurn(msg, authorId, content, taskThreadContext, attentionSnapshot, incomingParts) {
|
|
1994
2096
|
const executed = await this.executeTurn(msg, authorId, content, {
|
|
1995
2097
|
attentionSnapshot,
|
|
1996
2098
|
taskThreadContext,
|
|
2099
|
+
incomingParts,
|
|
1997
2100
|
});
|
|
1998
2101
|
if (executed.completed && executed.reply) {
|
|
1999
2102
|
this.updateAwaitingUserState(msg.channel_id, executed.reply.awaitingUser);
|
|
@@ -2086,6 +2189,11 @@ export class AgentsHost {
|
|
|
2086
2189
|
: undefined;
|
|
2087
2190
|
const missedCollaborationDiagnostic = this.resolveMissedCollaborationDiagnosticForTurn(channelId);
|
|
2088
2191
|
const collaborationCapabilities = this.resolveCollaborationCapabilityDeclarationForTurn();
|
|
2192
|
+
const incomingParts = options?.incomingParts
|
|
2193
|
+
?? buildHostedTurnContentParts({
|
|
2194
|
+
text: String(msg.content ?? msg.body ?? content),
|
|
2195
|
+
attachments: msg.attachments,
|
|
2196
|
+
});
|
|
2089
2197
|
try {
|
|
2090
2198
|
reply = await this.provider.generateReply({
|
|
2091
2199
|
agentName: this.config.agent.agentName,
|
|
@@ -2093,6 +2201,7 @@ export class AgentsHost {
|
|
|
2093
2201
|
channelId,
|
|
2094
2202
|
incomingAuthorId: authorId,
|
|
2095
2203
|
incomingContent: content,
|
|
2204
|
+
incomingParts,
|
|
2096
2205
|
incomingEventKind: msg.type,
|
|
2097
2206
|
incomingMessageType: msg.message_type,
|
|
2098
2207
|
collaboration: turnExecutionId || options?.kickoff
|
|
@@ -2126,6 +2235,11 @@ export class AgentsHost {
|
|
|
2126
2235
|
: undefined);
|
|
2127
2236
|
publicReplyText = resolvePublicReplyText(reply);
|
|
2128
2237
|
visibleReplyBody = this.resolveVisibleTurnBody(activeTurn, reply, publicReplyText);
|
|
2238
|
+
// NOT awaited: nothing downstream reads the result, and awaiting it here
|
|
2239
|
+
// widens the window before the staleness check below, where a slow write
|
|
2240
|
+
// can get an already-finished reply discarded. The method swallows its own
|
|
2241
|
+
// errors, so there is nothing for a caller to handle.
|
|
2242
|
+
void this.recordAgentSessionOnTask(channelId, taskThreadContext, reply.sessionId);
|
|
2129
2243
|
const appliedAttentionSnapshot = this.applyAttentionUpdate(channelId, attentionSnapshot, reply.controlMalformed
|
|
2130
2244
|
? undefined
|
|
2131
2245
|
: (reply.control && 'attentionUpdate' in reply.control
|
|
@@ -2631,17 +2745,22 @@ export class AgentsHost {
|
|
|
2631
2745
|
});
|
|
2632
2746
|
return null;
|
|
2633
2747
|
}
|
|
2634
|
-
const
|
|
2635
|
-
|
|
2748
|
+
const incomingParts = buildHostedTurnContentParts({
|
|
2749
|
+
text: String(msg.content ?? msg.body ?? ''),
|
|
2750
|
+
attachments: msg.attachments,
|
|
2751
|
+
});
|
|
2752
|
+
if (incomingParts.length === 0) {
|
|
2636
2753
|
this.logger.debug('ignored empty message', {
|
|
2637
2754
|
channelId: msg.channel_id,
|
|
2638
2755
|
messageId: msg.message_id,
|
|
2639
2756
|
});
|
|
2640
2757
|
return null;
|
|
2641
2758
|
}
|
|
2759
|
+
const content = buildHostedIncomingContentText(incomingParts);
|
|
2642
2760
|
return {
|
|
2643
2761
|
authorId,
|
|
2644
2762
|
content,
|
|
2763
|
+
incomingParts,
|
|
2645
2764
|
};
|
|
2646
2765
|
}
|
|
2647
2766
|
// Only a task assigned to this agent drives a task-assignment turn: it keeps the message a
|
|
@@ -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 {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createBorgeePlugin, } from '../plugin-sdk.js';
|
|
2
|
+
import { normalizeHostedMessageAttachment } from '../hosted-turn-content.js';
|
|
2
3
|
/**
|
|
3
4
|
* Thin adapter over `@borgee/plugin-sdk` (the same BPP/`/ws/plugin` SDK used by
|
|
4
5
|
* the OpenClaw plugin) that implements the minimal `ChatControlPlane` surface
|
|
@@ -97,6 +98,7 @@ export class SdkChatControlPlane {
|
|
|
97
98
|
authorId: message.authorId,
|
|
98
99
|
body: message.body,
|
|
99
100
|
contentType: message.contentType,
|
|
101
|
+
attachments: message.attachments?.map(normalizeHostedMessageAttachment),
|
|
100
102
|
type: message.type,
|
|
101
103
|
replyToId: message.replyToId,
|
|
102
104
|
createdAt: message.createdAt,
|
|
@@ -123,6 +125,12 @@ export class SdkChatControlPlane {
|
|
|
123
125
|
async updateTask(input) {
|
|
124
126
|
return this.client.updateTask(input);
|
|
125
127
|
}
|
|
128
|
+
async setTaskProperty(input) {
|
|
129
|
+
return this.client.setTaskProperty(input);
|
|
130
|
+
}
|
|
131
|
+
async deleteTaskProperty(input) {
|
|
132
|
+
return this.client.deleteTaskProperty(input);
|
|
133
|
+
}
|
|
126
134
|
}
|
|
127
135
|
export function mapInboundToChannelMessage(event) {
|
|
128
136
|
return {
|
|
@@ -136,6 +144,7 @@ export function mapInboundToChannelMessage(event) {
|
|
|
136
144
|
content: event.message?.body,
|
|
137
145
|
body: event.message?.body,
|
|
138
146
|
content_type: event.message?.contentType,
|
|
147
|
+
attachments: event.message?.attachments?.map(normalizeHostedMessageAttachment),
|
|
139
148
|
created_at: event.createdAt,
|
|
140
149
|
};
|
|
141
150
|
}
|