@animalabs/agent-framework 0.21.1 → 0.22.0
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 +43 -0
- package/dist/src/agent.d.ts +17 -4
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +102 -42
- package/dist/src/agent.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +22 -1
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +64 -9
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +4 -0
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +215 -38
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/framework.d.ts +250 -39
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1602 -371
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +21 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +40 -7
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +26 -3
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +28 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +17 -0
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +11 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +31 -7
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +2 -0
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +1 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +27 -6
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +85 -22
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +32 -4
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +46 -13
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +5 -3
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +41 -11
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +4 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +45 -8
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +5 -2
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +25 -7
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts +52 -2
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js +47 -1
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/tool-pattern-check.d.ts +51 -0
- package/dist/src/mcpl/tool-pattern-check.d.ts.map +1 -0
- package/dist/src/mcpl/tool-pattern-check.js +79 -0
- package/dist/src/mcpl/tool-pattern-check.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +15 -5
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +39 -0
- package/dist/src/mcpl/visible-content.d.ts.map +1 -0
- package/dist/src/mcpl/visible-content.js +54 -0
- package/dist/src/mcpl/visible-content.js.map +1 -0
- package/dist/src/module-registry.d.ts +81 -3
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +257 -16
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/workspace/branch-intent.d.ts +72 -0
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -0
- package/dist/src/modules/workspace/branch-intent.js +83 -0
- package/dist/src/modules/workspace/branch-intent.js.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts +167 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.js +259 -0
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -0
- package/dist/src/modules/workspace/effects.d.ts +84 -0
- package/dist/src/modules/workspace/effects.d.ts.map +1 -0
- package/dist/src/modules/workspace/effects.js +368 -0
- package/dist/src/modules/workspace/effects.js.map +1 -0
- package/dist/src/modules/workspace/index.d.ts +155 -17
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +1059 -424
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +278 -0
- package/dist/src/modules/workspace/observe.d.ts.map +1 -0
- package/dist/src/modules/workspace/observe.js +805 -0
- package/dist/src/modules/workspace/observe.js.map +1 -0
- package/dist/src/modules/workspace/reconcile.d.ts +224 -0
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -0
- package/dist/src/modules/workspace/reconcile.js +901 -0
- package/dist/src/modules/workspace/reconcile.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +6 -59
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +6 -257
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +77 -10
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/record-journal.d.ts +127 -0
- package/dist/src/record-journal.d.ts.map +1 -0
- package/dist/src/record-journal.js +202 -0
- package/dist/src/record-journal.js.map +1 -0
- package/dist/src/shutdown.d.ts +13 -0
- package/dist/src/shutdown.d.ts.map +1 -0
- package/dist/src/shutdown.js +46 -0
- package/dist/src/shutdown.js.map +1 -0
- package/dist/src/silent-heartbeat.d.ts +74 -0
- package/dist/src/silent-heartbeat.d.ts.map +1 -0
- package/dist/src/silent-heartbeat.js +77 -0
- package/dist/src/silent-heartbeat.js.map +1 -0
- package/dist/src/tool-presentation.d.ts +50 -0
- package/dist/src/tool-presentation.d.ts.map +1 -0
- package/dist/src/tool-presentation.js +235 -0
- package/dist/src/tool-presentation.js.map +1 -0
- package/dist/src/tool-result-guard.d.ts +125 -6
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +310 -35
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +13 -0
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +21 -1
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/agent.d.ts +18 -2
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +4 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +12 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +14 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +73 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/batched-wake-locus.test.d.ts +2 -0
- package/dist/test/batched-wake-locus.test.d.ts.map +1 -0
- package/dist/test/batched-wake-locus.test.js +166 -0
- package/dist/test/batched-wake-locus.test.js.map +1 -0
- package/dist/test/code-execution.test.js +386 -2
- package/dist/test/code-execution.test.js.map +1 -1
- package/dist/test/component-defaults.test.d.ts +2 -0
- package/dist/test/component-defaults.test.d.ts.map +1 -0
- package/dist/test/component-defaults.test.js +54 -0
- package/dist/test/component-defaults.test.js.map +1 -0
- package/dist/test/conversation-routing.test.js +57 -1
- package/dist/test/conversation-routing.test.js.map +1 -1
- package/dist/test/ephemeral-settle.test.js +3 -0
- package/dist/test/ephemeral-settle.test.js.map +1 -1
- package/dist/test/event-gate.test.js +118 -0
- package/dist/test/event-gate.test.js.map +1 -1
- package/dist/test/explicit-prose-routing.test.js +977 -0
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/helpers/pytc-stop-rows.d.ts +11 -0
- package/dist/test/helpers/pytc-stop-rows.d.ts.map +1 -0
- package/dist/test/helpers/pytc-stop-rows.js +143 -0
- package/dist/test/helpers/pytc-stop-rows.js.map +1 -0
- package/dist/test/mcpl-coalescing.test.js +19 -6
- package/dist/test/mcpl-coalescing.test.js.map +1 -1
- package/dist/test/mcpl-config-diagnostics.test.d.ts +2 -0
- package/dist/test/mcpl-config-diagnostics.test.d.ts.map +1 -0
- package/dist/test/mcpl-config-diagnostics.test.js +449 -0
- package/dist/test/mcpl-config-diagnostics.test.js.map +1 -0
- package/dist/test/mcpl-empty-content.test.d.ts +2 -0
- package/dist/test/mcpl-empty-content.test.d.ts.map +1 -0
- package/dist/test/mcpl-empty-content.test.js +368 -0
- package/dist/test/mcpl-empty-content.test.js.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js +106 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +2 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.js +288 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -0
- package/dist/test/prose-stream-router.test.js +41 -0
- package/dist/test/prose-stream-router.test.js.map +1 -1
- package/dist/test/record-journal.test.d.ts +6 -0
- package/dist/test/record-journal.test.d.ts.map +1 -0
- package/dist/test/record-journal.test.js +269 -0
- package/dist/test/record-journal.test.js.map +1 -0
- package/dist/test/save-recent-image.test.js +102 -0
- package/dist/test/save-recent-image.test.js.map +1 -1
- package/dist/test/shutdown-errors.test.d.ts +2 -0
- package/dist/test/shutdown-errors.test.d.ts.map +1 -0
- package/dist/test/shutdown-errors.test.js +662 -0
- package/dist/test/shutdown-errors.test.js.map +1 -0
- package/dist/test/silent-heartbeat-rows.test.d.ts +2 -0
- package/dist/test/silent-heartbeat-rows.test.d.ts.map +1 -0
- package/dist/test/silent-heartbeat-rows.test.js +451 -0
- package/dist/test/silent-heartbeat-rows.test.js.map +1 -0
- package/dist/test/silent-heartbeat.test.js +49 -38
- package/dist/test/silent-heartbeat.test.js.map +1 -1
- package/dist/test/stop-with-work-in-flight.test.d.ts +23 -0
- package/dist/test/stop-with-work-in-flight.test.d.ts.map +1 -0
- package/dist/test/stop-with-work-in-flight.test.js +672 -0
- package/dist/test/stop-with-work-in-flight.test.js.map +1 -0
- package/dist/test/tool-batch-hook.test.d.ts +8 -0
- package/dist/test/tool-batch-hook.test.d.ts.map +1 -0
- package/dist/test/tool-batch-hook.test.js +275 -0
- package/dist/test/tool-batch-hook.test.js.map +1 -0
- package/dist/test/tool-presentation.test.d.ts +2 -0
- package/dist/test/tool-presentation.test.d.ts.map +1 -0
- package/dist/test/tool-presentation.test.js +268 -0
- package/dist/test/tool-presentation.test.js.map +1 -0
- package/dist/test/tool-result-guard.test.js +2330 -18
- package/dist/test/tool-result-guard.test.js.map +1 -1
- package/dist/test/tool-result-history.test.js +2 -1
- package/dist/test/tool-result-history.test.js.map +1 -1
- package/dist/test/turn-trigger-provenance.test.js +35 -0
- package/dist/test/turn-trigger-provenance.test.js.map +1 -1
- package/dist/test/unconvertible-tool-result.test.d.ts +2 -0
- package/dist/test/unconvertible-tool-result.test.d.ts.map +1 -0
- package/dist/test/unconvertible-tool-result.test.js +358 -0
- package/dist/test/unconvertible-tool-result.test.js.map +1 -0
- package/dist/test/workspace-materialize-freshness.test.d.ts +16 -0
- package/dist/test/workspace-materialize-freshness.test.d.ts.map +1 -0
- package/dist/test/workspace-materialize-freshness.test.js +252 -0
- package/dist/test/workspace-materialize-freshness.test.js.map +1 -0
- package/dist/test/workspace-three-way.test.d.ts +12 -0
- package/dist/test/workspace-three-way.test.d.ts.map +1 -0
- package/dist/test/workspace-three-way.test.js +3079 -0
- package/dist/test/workspace-three-way.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/agent.ts +120 -44
- package/src/code-execution/py-runner.ts +66 -9
- package/src/code-execution/runtime-py.ts +215 -38
- package/src/framework.ts +1661 -363
- package/src/gate/event-gate.ts +59 -7
- package/src/index.ts +11 -0
- package/src/mcpl/channel-registry.ts +24 -3
- package/src/mcpl/conversation-router.ts +34 -1
- package/src/mcpl/feature-set-manager.ts +36 -11
- package/src/mcpl/index.ts +2 -0
- package/src/mcpl/prose-stream-router.ts +79 -23
- package/src/mcpl/push-coalescer.ts +62 -13
- package/src/mcpl/push-handler.ts +41 -12
- package/src/mcpl/server-connection.ts +41 -10
- package/src/mcpl/server-registry.ts +24 -7
- package/src/mcpl/tool-lifecycle.ts +77 -2
- package/src/mcpl/tool-pattern-check.ts +106 -0
- package/src/mcpl/types.ts +15 -5
- package/src/mcpl/visible-content.ts +57 -0
- package/src/module-registry.ts +252 -16
- package/src/modules/workspace/branch-intent.ts +123 -0
- package/src/modules/workspace/disk-agreement.ts +325 -0
- package/src/modules/workspace/effects.ts +362 -0
- package/src/modules/workspace/index.ts +1058 -446
- package/src/modules/workspace/observe.ts +839 -0
- package/src/modules/workspace/reconcile.ts +1136 -0
- package/src/modules/workspace/sync.ts +6 -336
- package/src/modules/workspace/types.ts +75 -10
- package/src/record-journal.ts +246 -0
- package/src/shutdown.ts +49 -0
- package/src/silent-heartbeat.ts +141 -0
- package/src/tool-presentation.ts +188 -0
- package/src/tool-result-guard.ts +335 -33
- package/src/tool-result-history.ts +25 -1
- package/src/types/agent.ts +18 -2
- package/src/types/events.ts +4 -0
- package/src/types/framework.ts +12 -0
- package/src/types/module.ts +15 -0
- package/src/types/trace.ts +80 -0
package/src/tool-result-guard.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
-
import type { ContextManager, MessageId } from '@animalabs/context-manager';
|
|
3
|
+
import type { ContextManager, MessageId, MessageMetadata } from '@animalabs/context-manager';
|
|
4
4
|
import type { ContentBlock, NormalizedMessage, ToolResult } from '@animalabs/membrane';
|
|
5
5
|
import type { CompletedToolCall } from './types/index.js';
|
|
6
6
|
import { isStateExistsError } from './module-registry.js';
|
|
@@ -8,6 +8,77 @@ import { isStateExistsError } from './module-registry.js';
|
|
|
8
8
|
export const TOOL_RESULT_GUARD_NOTICE = 'Tool result withheld by the guard. The tool has already executed.';
|
|
9
9
|
export const TOOL_RESULT_GUARD_AUDIT_STATE = 'framework/tool-result-guard';
|
|
10
10
|
|
|
11
|
+
/** Where a withheld result's original went: a workspace path; the path a
|
|
12
|
+
* write to it failed at (`error`); the path a write was still under way to
|
|
13
|
+
* when the host stopped waiting for it (`unfinished`); no write, because the
|
|
14
|
+
* workspace isn't answering (`workspace_unanswered`: an earlier write is
|
|
15
|
+
* still under way past the host's deadline) or because the workspace had
|
|
16
|
+
* left the stub's branch when the file would have been committed
|
|
17
|
+
* (`branch_changed`), or because the batch used its deadline before this
|
|
18
|
+
* write could start (`batch_deadline`); or null when no file is written at all (no writable
|
|
19
|
+
* workspace, or the host is stopping). */
|
|
20
|
+
export type WithheldSpill =
|
|
21
|
+
| { path: string; error?: string; unfinished?: true }
|
|
22
|
+
| { skipped: 'workspace_unanswered' | 'branch_changed' | 'batch_deadline' }
|
|
23
|
+
| null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Writes one withheld original as a workspace file, using what remains of
|
|
27
|
+
* the batch's absolute deadline. A write still running then is unfinished.
|
|
28
|
+
* Once a write reaches that deadline, later spills are workspace_unanswered
|
|
29
|
+
* until all overdue writes settle. Other batches can start before that gate
|
|
30
|
+
* closes. An exhausted batch starts no write (batch_deadline).
|
|
31
|
+
* The tree entry lands on branch or the write is declined (branch_changed).
|
|
32
|
+
* The mount's disk directory is shared across branches.
|
|
33
|
+
*/
|
|
34
|
+
export type WithheldSpiller = (label: string, text: string, branch: string, deadline: number) => Promise<WithheldSpill>;
|
|
35
|
+
|
|
36
|
+
/** What a host gives a guard for its withheld results: where originals are
|
|
37
|
+
* written, where each annotation is registered, so the host's stop can
|
|
38
|
+
* wait for it even after the agent itself was disposed, and where the
|
|
39
|
+
* guard says whether it has work left. */
|
|
40
|
+
export interface WithheldHost {
|
|
41
|
+
spill: WithheldSpiller;
|
|
42
|
+
/** Absolute deadline for one batch (Date.now() time), taken when the batch is queued. */
|
|
43
|
+
deadline(): number;
|
|
44
|
+
track(annotation: Promise<void>): void;
|
|
45
|
+
/** Whether the guard is idle (see ToolResultGuard.idle), told after each
|
|
46
|
+
* change that can alter it: when an annotation starts or ends, and after
|
|
47
|
+
* each pass over the storage queue. A host keeps a guard that isn't idle
|
|
48
|
+
* within reach for retries, whatever has become of its agent, and lets
|
|
49
|
+
* an idle one go. */
|
|
50
|
+
idle(idle: boolean): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The stub a withheld result settles to: #159's neutral notice, then where
|
|
55
|
+
* the original is. It names no refusal and no category (#159 keeps those in
|
|
56
|
+
* operational logs), only the place, so reading it back is the agent's
|
|
57
|
+
* deliberate choice (agent-framework #277). The place holds the result as
|
|
58
|
+
* stored, which for one over the inline cap is its capped text, whose own
|
|
59
|
+
* note names the file with the whole output; so the stub says "its result",
|
|
60
|
+
* not its full result. A failed write's own error text
|
|
61
|
+
* stays in the audit record and the logs, not here. The audit
|
|
62
|
+
* record is said to keep the original only when it was saved: a staged
|
|
63
|
+
* record whose save failed is still queued in memory, and a restart before
|
|
64
|
+
* its retry loses it.
|
|
65
|
+
*/
|
|
66
|
+
export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean): string {
|
|
67
|
+
const audit = `the guard's audit record (${TOOL_RESULT_GUARD_AUDIT_STATE}), for an operator`;
|
|
68
|
+
const kept = (only: string) => auditDurable
|
|
69
|
+
? `is kept ${only}in ${audit}`
|
|
70
|
+
: `went ${only}to ${audit}, though saving that record had failed when the result arrived`;
|
|
71
|
+
if (!spill || 'skipped' in spill) return `${TOOL_RESULT_GUARD_NOTICE} Its result ${kept('')}.`;
|
|
72
|
+
if (spill.unfinished) {
|
|
73
|
+
return `${TOOL_RESULT_GUARD_NOTICE} Its result was still being written to workspace file ${spill.path} ` +
|
|
74
|
+
`when this note was made; if that file is missing, it ${kept('')}.`;
|
|
75
|
+
}
|
|
76
|
+
if (spill.error === undefined) {
|
|
77
|
+
return `${TOOL_RESULT_GUARD_NOTICE} Its result is in workspace file ${spill.path}.`;
|
|
78
|
+
}
|
|
79
|
+
return `${TOOL_RESULT_GUARD_NOTICE} Writing its result to workspace file ${spill.path} failed, so it ${kept('only ')}.`;
|
|
80
|
+
}
|
|
81
|
+
|
|
11
82
|
interface PendingBatch {
|
|
12
83
|
id: string;
|
|
13
84
|
messageId: MessageId;
|
|
@@ -43,6 +114,13 @@ export class ToolResultGuard {
|
|
|
43
114
|
private override: boolean | undefined;
|
|
44
115
|
/** True until a recovery produces a clean response/new tool round. */
|
|
45
116
|
recovering = false;
|
|
117
|
+
/** Writes withheld originals where the agent can read them; without it,
|
|
118
|
+
* a stub names only the audit record. */
|
|
119
|
+
private host: WithheldHost | undefined;
|
|
120
|
+
/** Annotations of withheld stubs, chained in settlement order. */
|
|
121
|
+
private annotating: Promise<void> | undefined;
|
|
122
|
+
/** Annotations started so far, so a read can tell one began during it. */
|
|
123
|
+
private annotationsStarted = 0;
|
|
46
124
|
|
|
47
125
|
constructor(
|
|
48
126
|
private readonly agentName: string,
|
|
@@ -51,6 +129,51 @@ export class ToolResultGuard {
|
|
|
51
129
|
) {}
|
|
52
130
|
|
|
53
131
|
get enabled(): boolean { return this.override ?? this.configured; }
|
|
132
|
+
setHost(host: WithheldHost | undefined): void { this.host = host; }
|
|
133
|
+
/** Settles once every annotation started so far has finished: each of
|
|
134
|
+
* those stubs has been edited to say where its original is, or its edit
|
|
135
|
+
* failed and waits in the storage queue, retried in order until it lands
|
|
136
|
+
* or an operator's change supersedes it (until then the stub is the bare
|
|
137
|
+
* notice, kept out of summaries by its compression hold, and the audit
|
|
138
|
+
* record names the place). It never rejects. Each batch has one deadline
|
|
139
|
+
* for all its workspace writes; queued batches are annotated in order.
|
|
140
|
+
* A request may carry an unfinished-write notice once that deadline passes. */
|
|
141
|
+
whenAnnotated(): Promise<void> { return this.annotating ?? Promise.resolve(); }
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Runs `read` (a compile of the context) once no withheld stub is still
|
|
145
|
+
* being annotated, and again if an annotation began while it ran, since a
|
|
146
|
+
* settlement can land during the compile's own awaits, such as a previous
|
|
147
|
+
* frame's teardown abandoning its batch. The framework's sent-request
|
|
148
|
+
* compiles go through it: the agent's activations, its direct
|
|
149
|
+
* API calls and their refusal retry, and a conversation fork's seed from its
|
|
150
|
+
* template. A request preview doesn't, so it never waits on a
|
|
151
|
+
* workspace write.
|
|
152
|
+
*
|
|
153
|
+
* Before `read`, a stub edit that failed gets another try with the rest of
|
|
154
|
+
* the storage queue; a store that is still failing then compiles the bare
|
|
155
|
+
* notice, which its hold keeps out of summaries.
|
|
156
|
+
*/
|
|
157
|
+
async afterAnnotations<T>(read: () => Promise<T>): Promise<T> {
|
|
158
|
+
for (;;) {
|
|
159
|
+
// Counted before the wait, so an annotation that starts while this one
|
|
160
|
+
// is awaited is waited for too.
|
|
161
|
+
const started = this.annotationsStarted;
|
|
162
|
+
await this.whenAnnotated();
|
|
163
|
+
if (this.annotationsStarted !== started) continue;
|
|
164
|
+
try {
|
|
165
|
+
this.flushUnrecorded();
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`[tool-result-guard] agent=${this.agentName} storage retry before a compile failed:`, error);
|
|
168
|
+
}
|
|
169
|
+
const result = await read();
|
|
170
|
+
if (this.annotationsStarted === started) return result;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** No annotation is running and no storage work is queued: nothing of this
|
|
175
|
+
* guard is left for a host to wait on or retry. */
|
|
176
|
+
get idle(): boolean { return this.annotating === undefined && this.unrecorded.length === 0; }
|
|
54
177
|
setOverride(value: boolean | undefined): void { this.override = value; }
|
|
55
178
|
get settingOverride(): boolean | undefined { return this.override; }
|
|
56
179
|
get hasPending(): boolean { return this.pending !== undefined; }
|
|
@@ -81,39 +204,52 @@ export class ToolResultGuard {
|
|
|
81
204
|
|
|
82
205
|
/** Ordered storage work, separate from batches awaiting a provider verdict.
|
|
83
206
|
* A failed acceptance edit retains both the payload and its compression
|
|
84
|
-
* hold here; it can never re-arm refusal handling or tool submission.
|
|
207
|
+
* hold here; it can never re-arm refusal handling or tool submission.
|
|
208
|
+
* Any failed head, including an audit-only record, blocks later queued
|
|
209
|
+
* edits and their holds until that operation succeeds. */
|
|
85
210
|
private unrecorded: AuditOperation[] = [];
|
|
86
211
|
|
|
87
|
-
/** Retry edits and audit writes before a new activation,
|
|
88
|
-
*
|
|
212
|
+
/** Retry edits and audit writes before a new activation, a compile of a
|
|
213
|
+
* request that is sent (afterAnnotations), an audit write, a maintenance
|
|
214
|
+
* pass, or shutdown. A failed operation stays at the head. */
|
|
89
215
|
flushUnrecorded(): void {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// message and then failed while propagating to context entries.
|
|
100
|
-
// Chronicle normalizes object-key order and drops undefined fields.
|
|
101
|
-
if (!sameBranch || !current ||
|
|
102
|
-
(!isDeepStrictEqual(current.content, JSON.parse(JSON.stringify(expected)))
|
|
103
|
-
&& !isDeepStrictEqual(current.content, JSON.parse(JSON.stringify(content))))) {
|
|
104
|
-
operation.record.historyUpdate = 'superseded';
|
|
105
|
-
} else {
|
|
106
|
-
this.cm.editMessage(messageId, content);
|
|
107
|
-
}
|
|
108
|
-
// An audit-only retry must not repeat an edit that already succeeded.
|
|
109
|
-
operation.admission = undefined;
|
|
216
|
+
try {
|
|
217
|
+
while (this.unrecorded.length > 0) {
|
|
218
|
+
const operation = this.unrecorded[0];
|
|
219
|
+
try {
|
|
220
|
+
this.applyEdit(operation);
|
|
221
|
+
this.write(operation.record);
|
|
222
|
+
this.unrecorded.shift();
|
|
223
|
+
} finally {
|
|
224
|
+
if (!operation.admission) this.releaseOperationHold(operation);
|
|
110
225
|
}
|
|
111
|
-
this.write(operation.record);
|
|
112
|
-
this.unrecorded.shift();
|
|
113
|
-
} finally {
|
|
114
|
-
if (!operation.admission) this.releaseOperationHold(operation);
|
|
115
226
|
}
|
|
227
|
+
} finally {
|
|
228
|
+
this.host?.idle(this.idle);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Make an operation's history edit, if it still has one: once it is made,
|
|
233
|
+
* or superseded by an operator's change, only its record is left to
|
|
234
|
+
* write, and its hold can go. */
|
|
235
|
+
private applyEdit(operation: AuditOperation): void {
|
|
236
|
+
if (!operation.admission) return;
|
|
237
|
+
const { messageId, content, expected, branch } = operation.admission;
|
|
238
|
+
const sameBranch = this.cm.currentBranch().name === branch;
|
|
239
|
+
const current = sameBranch ? this.cm.getMessage(messageId) : null;
|
|
240
|
+
// A retry must not undo an operator's later edit/removal/rollback.
|
|
241
|
+
// Reapplying our own payload is safe if an earlier edit wrote the
|
|
242
|
+
// message and then failed while propagating to context entries.
|
|
243
|
+
// Chronicle normalizes object-key order and drops undefined fields.
|
|
244
|
+
if (!sameBranch || !current ||
|
|
245
|
+
(!isDeepStrictEqual(current.content, JSON.parse(JSON.stringify(expected)))
|
|
246
|
+
&& !isDeepStrictEqual(current.content, JSON.parse(JSON.stringify(content))))) {
|
|
247
|
+
operation.record.historyUpdate = 'superseded';
|
|
248
|
+
} else {
|
|
249
|
+
this.cm.editMessage(messageId, content);
|
|
116
250
|
}
|
|
251
|
+
// An audit-only retry must not repeat an edit that already succeeded.
|
|
252
|
+
operation.admission = undefined;
|
|
117
253
|
}
|
|
118
254
|
|
|
119
255
|
private append(record: Record<string, unknown>): void {
|
|
@@ -144,8 +280,13 @@ export class ToolResultGuard {
|
|
|
144
280
|
: JSON.parse(json);
|
|
145
281
|
}
|
|
146
282
|
|
|
147
|
-
storeResults(
|
|
148
|
-
|
|
283
|
+
storeResults(
|
|
284
|
+
content: ContentBlock[],
|
|
285
|
+
wireResults: ToolResult[],
|
|
286
|
+
originals: CompletedToolCall[],
|
|
287
|
+
metadata?: MessageMetadata,
|
|
288
|
+
): MessageId {
|
|
289
|
+
if (!this.enabled) return this.cm.addMessage('user', content, metadata);
|
|
149
290
|
if (this.pending) {
|
|
150
291
|
// Idempotent for the same not-yet-submitted batch: a caller that
|
|
151
292
|
// retries after a failure between staging and submission (e.g. a
|
|
@@ -175,7 +316,7 @@ export class ToolResultGuard {
|
|
|
175
316
|
// chunk is ever summarized from the placeholder; released after the
|
|
176
317
|
// settlement edit. Holds are in-memory only — a reopened manager has none,
|
|
177
318
|
// which is correct since a reopened guard has no pending batch.
|
|
178
|
-
const messageId = this.cm.addMessage('user', withheld,
|
|
319
|
+
const messageId = this.cm.addMessage('user', withheld, metadata, undefined, { holdCompression: true });
|
|
179
320
|
this.pending = { id, messageId, content, withheld, wireResults,
|
|
180
321
|
branch: this.cm.currentBranch().name, submitted: false, durable: false };
|
|
181
322
|
// Durability barrier: the audit must reach Chronicle's chain heads before
|
|
@@ -285,7 +426,9 @@ export class ToolResultGuard {
|
|
|
285
426
|
|
|
286
427
|
/** Transfer ownership from provider admission to an ordered storage job.
|
|
287
428
|
* Failure retains the accepted payload and its hold until the edit succeeds.
|
|
288
|
-
*
|
|
429
|
+
* A withheld batch's hold passes to its annotation instead, and from there
|
|
430
|
+
* to its stub's edit, so no summary is made of a stub that doesn't yet say
|
|
431
|
+
* where its original went. */
|
|
289
432
|
private settle(pending: PendingBatch, record: Record<string, unknown>, admit: boolean): void {
|
|
290
433
|
this.pending = undefined;
|
|
291
434
|
const operation: AuditOperation = {
|
|
@@ -293,8 +436,9 @@ export class ToolResultGuard {
|
|
|
293
436
|
batchId: pending.id, messageId: pending.messageId, sourceBranch: pending.branch },
|
|
294
437
|
...(admit ? { admission: { messageId: pending.messageId, content: pending.content,
|
|
295
438
|
expected: pending.withheld, branch: pending.branch } } : {}),
|
|
296
|
-
heldMessageId: pending.messageId,
|
|
439
|
+
heldMessageId: admit ? pending.messageId : undefined,
|
|
297
440
|
};
|
|
441
|
+
if (!admit) this.annotate(pending);
|
|
298
442
|
this.unrecorded.push(operation);
|
|
299
443
|
try {
|
|
300
444
|
this.flushUnrecorded();
|
|
@@ -305,6 +449,164 @@ export class ToolResultGuard {
|
|
|
305
449
|
}
|
|
306
450
|
}
|
|
307
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Turn a withheld batch's notices into stubs that say where each original
|
|
454
|
+
* is (agent-framework #277): written to the workspace when a writable mount
|
|
455
|
+
* takes it, else kept in the audit record. Its own unedited placeholder is
|
|
456
|
+
* the only thing it edits: an operator's later edit, removal or rollback
|
|
457
|
+
* stands.
|
|
458
|
+
*
|
|
459
|
+
* Where each original went is queued first, on its own ('annotated'), so
|
|
460
|
+
* a stub edit that fails can't take the record of a written file with it.
|
|
461
|
+
* The edit is then made at once, so no storage work queued ahead of it
|
|
462
|
+
* holds it, not even that record if its write is failing, and its record
|
|
463
|
+
* ('stub_updated') is written in order after the others. An
|
|
464
|
+
* edit that fails joins the ordered storage queue like an admission's, with
|
|
465
|
+
* the batch's compression hold: retried before the next activation, compile,
|
|
466
|
+
* maintenance pass or stop until it lands or an operator's change supersedes
|
|
467
|
+
* it. Until then the stub is the bare notice, which the hold keeps out of
|
|
468
|
+
* summaries, and the audit record names the file.
|
|
469
|
+
*/
|
|
470
|
+
private annotate(pending: PendingBatch): void {
|
|
471
|
+
const previous = this.annotating ?? Promise.resolve();
|
|
472
|
+
this.annotationsStarted += 1;
|
|
473
|
+
// The batch's deadline runs from when it's queued, not from when the
|
|
474
|
+
// batches ahead of it finish, so a request waiting on a chain of batches
|
|
475
|
+
// waits at most one deadline past the latest one's arrival.
|
|
476
|
+
let deadline = Infinity;
|
|
477
|
+
try { deadline = this.host?.deadline() ?? Infinity; } catch { /* the spill then takes its own deadline, rather than losing the record */ }
|
|
478
|
+
const run = previous.then(async () => {
|
|
479
|
+
// Set once the stub's edit is queued with the hold: the queue releases
|
|
480
|
+
// it when the edit lands or is superseded.
|
|
481
|
+
let holdQueued = false;
|
|
482
|
+
try {
|
|
483
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
484
|
+
// A file is named by its batch and its place in the batch, ahead of
|
|
485
|
+
// anything the writer may cut or substitute: results of one batch
|
|
486
|
+
// never share a file, and results of two batches share one only if 48
|
|
487
|
+
// random bits of their batch ids agree. The tool use id after them is
|
|
488
|
+
// for whoever lists the directory.
|
|
489
|
+
const batch = pending.id.replace(/-/g, '').slice(0, 12);
|
|
490
|
+
// Keyed by the result's index in the batch, which the stub and the
|
|
491
|
+
// original share.
|
|
492
|
+
const spills = new Map<number, WithheldSpill>();
|
|
493
|
+
// A spill that threw, by index: its error, for the record.
|
|
494
|
+
const thrown = new Map<number, string>();
|
|
495
|
+
// Set once the branch has changed: no more is written.
|
|
496
|
+
let branchLeft = false;
|
|
497
|
+
let deadlineSpent = false;
|
|
498
|
+
for (const [index, block] of pending.content.entries()) {
|
|
499
|
+
if (block.type !== 'tool_result') continue;
|
|
500
|
+
// Once the store has left the batch's branch (a rollback, an undo, a
|
|
501
|
+
// host's switch), write no more. This result and every later one is
|
|
502
|
+
// skipped, and its original stays in the audit record. Nothing
|
|
503
|
+
// yields from here to the edit's own branch check, so while the
|
|
504
|
+
// store is away the stub stays as it is. A write already under way
|
|
505
|
+
// when the branch changes is refused before its tree entry is set,
|
|
506
|
+
// since it names the batch's branch. Once set, that entry stays on
|
|
507
|
+
// its branch; its disk copy uses the mount's shared directory.
|
|
508
|
+
if (branchLeft || this.cm.currentBranch().name !== pending.branch) {
|
|
509
|
+
branchLeft = true;
|
|
510
|
+
spills.set(index, { skipped: 'branch_changed' });
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
// The result as stored. Both paths that store a tool result store
|
|
514
|
+
// its text; content stored as blocks would be written as their JSON.
|
|
515
|
+
const original = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
516
|
+
let spill: WithheldSpill = null;
|
|
517
|
+
if (this.host) {
|
|
518
|
+
try {
|
|
519
|
+
spill = await this.host.spill(`${date}-withheld-${batch}-${index}-${block.toolUseId}`, original, pending.branch, deadline);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
// The host reports a failed write rather than throwing; a throw
|
|
522
|
+
// is recorded all the same, without a path.
|
|
523
|
+
thrown.set(index, (error instanceof Error ? error.message : String(error)).slice(0, 200));
|
|
524
|
+
console.error(`[tool-result-guard] agent=${this.agentName} could not spill withheld result ` +
|
|
525
|
+
`${block.toolUseId}:`, error);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Declined where it would have committed, since the workspace had
|
|
529
|
+
// left the batch's branch. Nothing was written, and no more are, as
|
|
530
|
+
// for the results the branch check above stops. If the branch is
|
|
531
|
+
// back by the edit, as after an undo and a redo, the stubs of this
|
|
532
|
+
// result and the later ones name the audit record.
|
|
533
|
+
if (spill && 'skipped' in spill && spill.skipped === 'branch_changed') branchLeft = true;
|
|
534
|
+
// A batch that used its deadline is said once, here, where the
|
|
535
|
+
// batch is known; the host returns the skip without a line.
|
|
536
|
+
if (spill && 'skipped' in spill && spill.skipped === 'batch_deadline' && !deadlineSpent) {
|
|
537
|
+
deadlineSpent = true;
|
|
538
|
+
console.error(`[tool-result-guard] agent=${this.agentName} batch=${pending.id} used its deadline before all its originals were written; the rest name the audit record (batch_deadline)`);
|
|
539
|
+
}
|
|
540
|
+
spills.set(index, spill);
|
|
541
|
+
}
|
|
542
|
+
const annotated: ContentBlock[] = pending.withheld.map((block, index) => block.type === 'tool_result'
|
|
543
|
+
? { ...block, content: withheldResultNotice(spills.get(index) ?? null, pending.durable) }
|
|
544
|
+
: block);
|
|
545
|
+
try {
|
|
546
|
+
this.append({
|
|
547
|
+
type: 'annotated', batchId: pending.id, messageId: pending.messageId, sourceBranch: pending.branch,
|
|
548
|
+
results: pending.content.flatMap((block, index): Array<Record<string, string | true>> => {
|
|
549
|
+
if (block.type !== 'tool_result') return [];
|
|
550
|
+
if (thrown.has(index)) return [{ toolUseId: block.toolUseId, error: thrown.get(index)! }];
|
|
551
|
+
const spill = spills.get(index) ?? null;
|
|
552
|
+
// No file was written: no writable workspace, or the framework
|
|
553
|
+
// was stopping.
|
|
554
|
+
if (spill === null) return [{ toolUseId: block.toolUseId, skipped: 'no_workspace_file' }];
|
|
555
|
+
if ('skipped' in spill) return [{ toolUseId: block.toolUseId, skipped: spill.skipped }];
|
|
556
|
+
return [{ toolUseId: block.toolUseId, path: spill.path,
|
|
557
|
+
...(spill.error !== undefined ? { error: spill.error } : {}),
|
|
558
|
+
...(spill.unfinished ? { unfinished: true as const } : {}) }];
|
|
559
|
+
}),
|
|
560
|
+
});
|
|
561
|
+
} catch (error) {
|
|
562
|
+
// It stays queued and is retried in order with the storage work
|
|
563
|
+
// ahead of it; the stub's record is written after it.
|
|
564
|
+
console.error(`[tool-result-guard] agent=${this.agentName} batch=${pending.id} annotation record queued for retry:`, error);
|
|
565
|
+
}
|
|
566
|
+
const stub: AuditOperation = {
|
|
567
|
+
record: { agentName: this.agentName, timestamp: Date.now(), type: 'stub_updated',
|
|
568
|
+
batchId: pending.id, messageId: pending.messageId, sourceBranch: pending.branch },
|
|
569
|
+
admission: { messageId: pending.messageId, content: annotated, expected: pending.withheld, branch: pending.branch },
|
|
570
|
+
heldMessageId: pending.messageId,
|
|
571
|
+
};
|
|
572
|
+
try {
|
|
573
|
+
this.applyEdit(stub);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
console.error(`[tool-result-guard] agent=${this.agentName} batch=${pending.id} stub edit queued for retry:`, error);
|
|
576
|
+
}
|
|
577
|
+
this.unrecorded.push(stub);
|
|
578
|
+
holdQueued = true;
|
|
579
|
+
try {
|
|
580
|
+
// A made edit's hold goes now, whatever storage work is still
|
|
581
|
+
// queued ahead of its record.
|
|
582
|
+
if (!stub.admission) this.releaseOperationHold(stub);
|
|
583
|
+
this.flushUnrecorded();
|
|
584
|
+
} catch (error) {
|
|
585
|
+
console.error(`[tool-result-guard] agent=${this.agentName} batch=${pending.id} stub's storage work queued for retry:`, error);
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.error(`[tool-result-guard] agent=${this.agentName} batch=${pending.id} could not annotate withheld results:`, error);
|
|
589
|
+
} finally {
|
|
590
|
+
// Never rejects: a compile and stop() wait on this chain. With no
|
|
591
|
+
// stub edit queued, nothing else holds the batch.
|
|
592
|
+
if (!holdQueued) {
|
|
593
|
+
try {
|
|
594
|
+
this.releaseHold(pending.messageId);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
console.error(`[tool-result-guard] agent=${this.agentName} could not release a withheld batch's hold:`, error);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
this.annotating = run;
|
|
602
|
+
void run.finally(() => {
|
|
603
|
+
if (this.annotating === run) this.annotating = undefined;
|
|
604
|
+
this.host?.idle(this.idle);
|
|
605
|
+
});
|
|
606
|
+
this.host?.track(run);
|
|
607
|
+
this.host?.idle(false);
|
|
608
|
+
}
|
|
609
|
+
|
|
308
610
|
private releaseOperationHold(operation: AuditOperation): void {
|
|
309
611
|
if (operation.heldMessageId !== undefined) {
|
|
310
612
|
this.releaseHold(operation.heldMessageId);
|
|
@@ -52,10 +52,34 @@ export function toolResultDataToHistoryString(
|
|
|
52
52
|
options?: HistorySerializeOptions,
|
|
53
53
|
): string {
|
|
54
54
|
const fromArray = tryHistoryStringFromContentArray(data, options);
|
|
55
|
-
|
|
55
|
+
// JSON leaves out undefined, a function and a symbol: a result with no data
|
|
56
|
+
// is stored as no text, as an empty content array is.
|
|
57
|
+
const str = fromArray ?? (JSON.stringify(data) as string | undefined) ?? '';
|
|
56
58
|
return maxChars ? truncateForHistory(str, maxChars) : str;
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The text of a tool result that isn't an error result: its data, as history
|
|
63
|
+
* stores it, or, when it has no data, its error text. A tool can answer
|
|
64
|
+
* `{ success: false, error, isError: false }`, a failure it doesn't flag as
|
|
65
|
+
* an error for the model, and the model still reads what failed.
|
|
66
|
+
*
|
|
67
|
+
* Each field is read once: the direct Agent API passes its caller's own
|
|
68
|
+
* result here, unchecked, and a getter needn't answer the same way twice.
|
|
69
|
+
*/
|
|
70
|
+
export function nonErrorResultText(
|
|
71
|
+
result: { data?: unknown; error?: string },
|
|
72
|
+
maxChars?: number,
|
|
73
|
+
options?: HistorySerializeOptions,
|
|
74
|
+
): string {
|
|
75
|
+
const data = result.data;
|
|
76
|
+
if (data === undefined || data === null) {
|
|
77
|
+
const error = result.error;
|
|
78
|
+
if (typeof error === 'string' && error) return maxChars ? truncateForHistory(error, maxChars) : error;
|
|
79
|
+
}
|
|
80
|
+
return toolResultDataToHistoryString(data, maxChars, options);
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
/** Bounded copy of an arbitrary string with the standard truncation notice.
|
|
60
84
|
* Image slots past the cut are re-appended, never dropped (issue #104). */
|
|
61
85
|
export function truncateForHistory(str: string, maxChars: number): string {
|
package/src/types/agent.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ContentBlock, YieldingStream } from '@animalabs/membrane';
|
|
2
2
|
import type { ContextStrategy } from '@animalabs/context-manager';
|
|
3
3
|
import type { ToolCallId, ToolResult, ToolCall } from './events.js';
|
|
4
|
+
import type { SilentHeartbeatTick } from '../silent-heartbeat.js';
|
|
4
5
|
|
|
5
6
|
export type SameRoundThinkTextPolicy = 'public' | 'private';
|
|
6
7
|
export type SameRoundThinkTextPolicySource =
|
|
@@ -12,6 +13,7 @@ export type SameRoundThinkTextPolicySource =
|
|
|
12
13
|
* Configuration for an agent.
|
|
13
14
|
*/
|
|
14
15
|
export interface AgentConfig {
|
|
16
|
+
toolPresentation?: import("../tool-presentation.js").ToolPresentationConfig;
|
|
15
17
|
/** Unique name for this agent */
|
|
16
18
|
name: string;
|
|
17
19
|
|
|
@@ -305,6 +307,14 @@ export interface InferenceRequest {
|
|
|
305
307
|
/** Host-owned identity for withdrawal of an unconsumed coalesced wake. */
|
|
306
308
|
coalescingSubject?: string;
|
|
307
309
|
coalescingEventId?: string;
|
|
310
|
+
/** Host-owned: this wake's cause is an RFC-006 deferred batch, which has
|
|
311
|
+
* no model-visible content until it renders at the turn's assembly. */
|
|
312
|
+
coalescingBatch?: boolean;
|
|
313
|
+
/** Host-owned, set on a turn's trigger when EVERY request batched into it
|
|
314
|
+
* was a deferred-batch wake: these are their subjects. If assembly settles
|
|
315
|
+
* all of them without materializing content, the turn has no cause left
|
|
316
|
+
* and does not run. */
|
|
317
|
+
coalescingBatchSubjects?: string[];
|
|
308
318
|
agentName: string;
|
|
309
319
|
reason: string;
|
|
310
320
|
source: string;
|
|
@@ -339,8 +349,10 @@ export interface InferenceRequest {
|
|
|
339
349
|
* Where the wake came from, for telemetry ONLY — never a speech locus.
|
|
340
350
|
* Set by the EventGate for batched wakes (composite channel id of the
|
|
341
351
|
* chosen event when the event carried one; push-event raw ids are not
|
|
342
|
-
* used). Routing keeps reading `channelId`,
|
|
343
|
-
*
|
|
352
|
+
* used). Routing keeps reading `channelId`, set from registered ids: by
|
|
353
|
+
* the direct channel paths, and by a gate wake only when its chosen event
|
|
354
|
+
* addressed the agent and the host resolved that event's registered
|
|
355
|
+
* channel (ambient-only batches leave it unset).
|
|
344
356
|
*/
|
|
345
357
|
wakeChannelId?: string;
|
|
346
358
|
/** Timestamp of the event the wake provenance was taken from (ms). */
|
|
@@ -351,4 +363,8 @@ export interface InferenceRequest {
|
|
|
351
363
|
/** Ephemeral system-position prompt for this turn only. Never written to
|
|
352
364
|
* Chronicle; callers must supply bounded non-secret control text. */
|
|
353
365
|
ephemeralSystemPrompt?: string;
|
|
366
|
+
/** The authenticated silent heartbeat tick this turn answers. Every row the
|
|
367
|
+
* turn stores is stamped `metadata.silentHeartbeat` with it, and request
|
|
368
|
+
* builds render a request-only separator before the tick's first row. */
|
|
369
|
+
silentHeartbeat?: SilentHeartbeatTick;
|
|
354
370
|
}
|
package/src/types/events.ts
CHANGED
|
@@ -92,6 +92,10 @@ export interface ToolResultEvent {
|
|
|
92
92
|
agentName: string;
|
|
93
93
|
/** Module that handled the call */
|
|
94
94
|
moduleName: string;
|
|
95
|
+
/** The tool's name, as its call named it. The framework's own
|
|
96
|
+
* producers set it, so a result nothing can take any more (one arriving
|
|
97
|
+
* after stop()) is still logged by tool. */
|
|
98
|
+
toolName?: string;
|
|
95
99
|
/** Result of the tool call */
|
|
96
100
|
result: ToolResult;
|
|
97
101
|
}
|
package/src/types/framework.ts
CHANGED
|
@@ -237,6 +237,11 @@ export interface FrameworkConfig {
|
|
|
237
237
|
* effective class (first matching pattern wins); replaces, never merges
|
|
238
238
|
* with, what the providing server declared. Use it to correct or tighten a
|
|
239
239
|
* misclassed tool.
|
|
240
|
+
*
|
|
241
|
+
* MCPL tools are named `<toolPrefix>--<tool>`, and toolPrefix defaults to
|
|
242
|
+
* `mcpl--<serverId>`: server `search` without a toolPrefix needs
|
|
243
|
+
* `mcpl--search--*`, not `search--*`. A pattern that matches no tool is
|
|
244
|
+
* reported once its servers have listed their tools.
|
|
240
245
|
*/
|
|
241
246
|
toolClassOverrides?: Record<string, string[]>;
|
|
242
247
|
|
|
@@ -274,6 +279,13 @@ export interface FrameworkConfig {
|
|
|
274
279
|
* messages route to per-channel fork agents spawned from the template
|
|
275
280
|
* agent instead of the primary conversation. If omitted, behavior is
|
|
276
281
|
* unchanged (all messages go to the primary agent).
|
|
282
|
+
*
|
|
283
|
+
* @deprecated Per-channel conversation routing is deprecated and will be
|
|
284
|
+
* removed (anima-research/agent-framework#235). Its 'mention' bind/trigger
|
|
285
|
+
* rule reads `metadata.mentioned`, which not every channel server sets
|
|
286
|
+
* (discord-mcpl does not), so on those channels an @-mention neither binds
|
|
287
|
+
* a fork nor triggers a bound one. Routing still works for now; the
|
|
288
|
+
* framework logs one `[deprecated]` line at creation when this is set.
|
|
277
289
|
*/
|
|
278
290
|
conversations?: ConversationRouterConfig;
|
|
279
291
|
}
|
package/src/types/module.ts
CHANGED
|
@@ -109,6 +109,21 @@ export interface Module {
|
|
|
109
109
|
*/
|
|
110
110
|
gatherContext?(agentName: string): Promise<ContextInjection[]>;
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Called once an agent's tool batch has completed — when the round's last
|
|
114
|
+
* pending result arrives, before it is provided to the agent and before
|
|
115
|
+
* anything that continues the turn (re-inference, stream resume). The
|
|
116
|
+
* framework awaits it, bounded at 30 s and fail-open, so a module can
|
|
117
|
+
* bring its state up to date with what the batch's tools did (the
|
|
118
|
+
* workspace's `watch: 'on-agent-action'` mounts scan disk here) before the
|
|
119
|
+
* agent's next inference. It's awaited in the framework's event loop, so
|
|
120
|
+
* a slow hook holds every agent's event processing, not only this one's.
|
|
121
|
+
* A hook still running from an earlier batch is skipped, not called again.
|
|
122
|
+
* Not called for script-inner tool calls, nor when the framework fills in
|
|
123
|
+
* error results for a batch whose stream ended before its tools answered.
|
|
124
|
+
*/
|
|
125
|
+
onToolBatchComplete?(agentName: string): Promise<void>;
|
|
126
|
+
|
|
112
127
|
/**
|
|
113
128
|
* Declare extra settings for the synthesized `agent_settings` tool.
|
|
114
129
|
* Modules owning hot-tunable runtime state (e.g. a host settings module
|