@animalabs/agent-framework 0.19.0 → 0.21.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 +29 -0
- package/dist/src/agent.d.ts +22 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +128 -10
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/types.d.ts +1 -1
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +10 -1
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +27 -2
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts +21 -0
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +34 -0
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/framework.d.ts +153 -2
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1175 -76
- package/dist/src/framework.js.map +1 -1
- package/dist/src/index.d.ts +2 -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/capability-grant.d.ts +1 -1
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
- package/dist/src/mcpl/capability-grant.js +49 -5
- package/dist/src/mcpl/capability-grant.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +59 -2
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +106 -6
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +8 -6
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +12 -6
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +3 -0
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +3 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +318 -0
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -0
- package/dist/src/mcpl/push-coalescer.js +709 -0
- package/dist/src/mcpl/push-coalescer.js.map +1 -0
- package/dist/src/mcpl/push-handler.d.ts +7 -2
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +43 -4
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +9 -4
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +18 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +44 -1
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-classes.d.ts +71 -0
- package/dist/src/mcpl/tool-classes.d.ts.map +1 -0
- package/dist/src/mcpl/tool-classes.js +170 -0
- package/dist/src/mcpl/tool-classes.js.map +1 -0
- package/dist/src/mcpl/tool-glob.d.ts +16 -0
- package/dist/src/mcpl/tool-glob.d.ts.map +1 -0
- package/dist/src/mcpl/tool-glob.js +41 -0
- package/dist/src/mcpl/tool-glob.js.map +1 -0
- package/dist/src/mcpl/tool-lifecycle.d.ts +253 -0
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -0
- package/dist/src/mcpl/tool-lifecycle.js +607 -0
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +15 -0
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +65 -1
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +68 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js +6 -0
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/modules/discord/index.js +1 -1
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +85 -0
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +1019 -24
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +202 -0
- package/dist/src/modules/history/semantic.d.ts.map +1 -0
- package/dist/src/modules/history/semantic.js +418 -0
- package/dist/src/modules/history/semantic.js.map +1 -0
- package/dist/src/modules/index.d.ts +2 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +82 -0
- package/dist/src/tool-result-guard.d.ts.map +1 -0
- package/dist/src/tool-result-guard.js +306 -0
- package/dist/src/tool-result-guard.js.map +1 -0
- package/dist/src/types/agent.d.ts +14 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +21 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +17 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/channel-registry-routing.test.js +59 -0
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/code-execution.test.js +139 -0
- package/dist/test/code-execution.test.js.map +1 -1
- package/dist/test/conversation-router.test.js +33 -19
- package/dist/test/conversation-router.test.js.map +1 -1
- package/dist/test/conversation-routing.test.js +89 -0
- package/dist/test/conversation-routing.test.js.map +1 -1
- package/dist/test/helpers/coalescing-fixture.d.ts +72 -0
- package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -0
- package/dist/test/helpers/coalescing-fixture.js +145 -0
- package/dist/test/helpers/coalescing-fixture.js.map +1 -0
- package/dist/test/helpers/coalescing-kill-child.d.ts +2 -0
- package/dist/test/helpers/coalescing-kill-child.d.ts.map +1 -0
- package/dist/test/helpers/coalescing-kill-child.js +24 -0
- package/dist/test/helpers/coalescing-kill-child.js.map +1 -0
- package/dist/test/history-module-realstore.test.d.ts +2 -0
- package/dist/test/history-module-realstore.test.d.ts.map +1 -0
- package/dist/test/history-module-realstore.test.js +363 -0
- package/dist/test/history-module-realstore.test.js.map +1 -0
- package/dist/test/history-module-semantic.test.d.ts +2 -0
- package/dist/test/history-module-semantic.test.d.ts.map +1 -0
- package/dist/test/history-module-semantic.test.js +213 -0
- package/dist/test/history-module-semantic.test.js.map +1 -0
- package/dist/test/history-module-ux.test.d.ts +2 -0
- package/dist/test/history-module-ux.test.d.ts.map +1 -0
- package/dist/test/history-module-ux.test.js +304 -0
- package/dist/test/history-module-ux.test.js.map +1 -0
- package/dist/test/history-module.test.js +25 -1
- package/dist/test/history-module.test.js.map +1 -1
- package/dist/test/history-semantic-review-findings.test.d.ts +2 -0
- package/dist/test/history-semantic-review-findings.test.d.ts.map +1 -0
- package/dist/test/history-semantic-review-findings.test.js +338 -0
- package/dist/test/history-semantic-review-findings.test.js.map +1 -0
- package/dist/test/history-semantic-sync-converges.test.d.ts +2 -0
- package/dist/test/history-semantic-sync-converges.test.d.ts.map +1 -0
- package/dist/test/history-semantic-sync-converges.test.js +109 -0
- package/dist/test/history-semantic-sync-converges.test.js.map +1 -0
- package/dist/test/mcpl-capability-grant.test.js +3 -1
- package/dist/test/mcpl-capability-grant.test.js.map +1 -1
- package/dist/test/mcpl-child-env.test.d.ts +2 -0
- package/dist/test/mcpl-child-env.test.d.ts.map +1 -0
- package/dist/test/mcpl-child-env.test.js +158 -0
- package/dist/test/mcpl-child-env.test.js.map +1 -0
- package/dist/test/mcpl-coalescing.test.d.ts +2 -0
- package/dist/test/mcpl-coalescing.test.d.ts.map +1 -0
- package/dist/test/mcpl-coalescing.test.js +1050 -0
- package/dist/test/mcpl-coalescing.test.js.map +1 -0
- package/dist/test/mcpl-list-status.test.js +4 -0
- package/dist/test/mcpl-list-status.test.js.map +1 -1
- package/dist/test/mcpl-reference-fields.test.d.ts +2 -0
- package/dist/test/mcpl-reference-fields.test.d.ts.map +1 -0
- package/dist/test/mcpl-reference-fields.test.js +82 -0
- package/dist/test/mcpl-reference-fields.test.js.map +1 -0
- package/dist/test/mcpl-tool-class-listing.test.d.ts +2 -0
- package/dist/test/mcpl-tool-class-listing.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-class-listing.test.js +147 -0
- package/dist/test/mcpl-tool-class-listing.test.js.map +1 -0
- package/dist/test/mcpl-tool-classes.test.d.ts +7 -0
- package/dist/test/mcpl-tool-classes.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-classes.test.js +80 -0
- package/dist/test/mcpl-tool-classes.test.js.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts +2 -0
- package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js +237 -0
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts +11 -0
- package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js +307 -0
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -0
- package/dist/test/mcpl-tool-lifecycle.test.d.ts +11 -0
- package/dist/test/mcpl-tool-lifecycle.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-lifecycle.test.js +270 -0
- package/dist/test/mcpl-tool-lifecycle.test.js.map +1 -0
- package/dist/test/silent-heartbeat.test.d.ts +2 -0
- package/dist/test/silent-heartbeat.test.d.ts.map +1 -0
- package/dist/test/silent-heartbeat.test.js +154 -0
- package/dist/test/silent-heartbeat.test.js.map +1 -0
- package/dist/test/tool-result-guard.test.d.ts +2 -0
- package/dist/test/tool-result-guard.test.d.ts.map +1 -0
- package/dist/test/tool-result-guard.test.js +1081 -0
- package/dist/test/tool-result-guard.test.js.map +1 -0
- package/dist/test/tools-observe-parser-conformance.test.d.ts +2 -0
- package/dist/test/tools-observe-parser-conformance.test.d.ts.map +1 -0
- package/dist/test/tools-observe-parser-conformance.test.js +129 -0
- package/dist/test/tools-observe-parser-conformance.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +67 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/agent.ts +128 -10
- package/src/api/types.ts +2 -0
- package/src/code-execution/py-runner.ts +34 -2
- package/src/code-execution/tool-definition.ts +46 -0
- package/src/framework.ts +1207 -80
- package/src/index.ts +13 -0
- package/src/mcpl/capability-grant.ts +54 -6
- package/src/mcpl/channel-registry.ts +115 -7
- package/src/mcpl/conversation-router.ts +12 -8
- package/src/mcpl/index.ts +29 -0
- package/src/mcpl/push-coalescer.ts +821 -0
- package/src/mcpl/push-handler.ts +40 -4
- package/src/mcpl/references.ts +11 -3
- package/src/mcpl/server-connection.ts +48 -2
- package/src/mcpl/tool-classes.ts +188 -0
- package/src/mcpl/tool-glob.ts +36 -0
- package/src/mcpl/tool-lifecycle.ts +745 -0
- package/src/mcpl/transport.ts +67 -1
- package/src/mcpl/types.ts +71 -1
- package/src/modules/discord/index.ts +1 -1
- package/src/modules/history/index.ts +1089 -24
- package/src/modules/history/semantic.ts +473 -0
- package/src/modules/index.ts +2 -1
- package/src/tool-result-guard.ts +318 -0
- package/src/types/agent.ts +15 -0
- package/src/types/framework.ts +23 -0
- package/src/types/trace.ts +17 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import type { ContextManager, MessageId } from '@animalabs/context-manager';
|
|
4
|
+
import type { ContentBlock, NormalizedMessage, ToolResult } from '@animalabs/membrane';
|
|
5
|
+
import type { CompletedToolCall } from './types/index.js';
|
|
6
|
+
import { isStateExistsError } from './module-registry.js';
|
|
7
|
+
|
|
8
|
+
export const TOOL_RESULT_GUARD_NOTICE = 'Tool result withheld by the guard. The tool has already executed.';
|
|
9
|
+
export const TOOL_RESULT_GUARD_AUDIT_STATE = 'framework/tool-result-guard';
|
|
10
|
+
|
|
11
|
+
interface PendingBatch {
|
|
12
|
+
id: string;
|
|
13
|
+
messageId: MessageId;
|
|
14
|
+
content: ContentBlock[];
|
|
15
|
+
withheld: ContentBlock[];
|
|
16
|
+
branch: string;
|
|
17
|
+
wireResults: ToolResult[];
|
|
18
|
+
submitted: boolean;
|
|
19
|
+
/** False when the audit sync failed: originals then never go on the wire. */
|
|
20
|
+
durable: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface AuditOperation {
|
|
24
|
+
record: Record<string, unknown>;
|
|
25
|
+
/** Acceptance must finish this edit before its outcome can be recorded. */
|
|
26
|
+
admission?: { messageId: MessageId; content: ContentBlock[]; expected: ContentBlock[]; branch: string };
|
|
27
|
+
/** Retained while an acceptance edit needs retrying, not for audit-only retries. */
|
|
28
|
+
heldMessageId?: MessageId;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Admission of newly returned tool output to durable model-facing memory.
|
|
33
|
+
*
|
|
34
|
+
* Raw output is appended to a separate Chronicle audit slot BEFORE the
|
|
35
|
+
* placeholder enters the context manager. In particular, onNewMessage and
|
|
36
|
+
* speculative compression never see unaccepted output. Acceptance edits the
|
|
37
|
+
* placeholder through CM's versioned edit API; withholding only appends an
|
|
38
|
+
* audit event. Neither operation erases the original output or its blobs.
|
|
39
|
+
*/
|
|
40
|
+
export class ToolResultGuard {
|
|
41
|
+
private pending: PendingBatch | undefined;
|
|
42
|
+
private registered = false;
|
|
43
|
+
private override: boolean | undefined;
|
|
44
|
+
/** True until a recovery produces a clean response/new tool round. */
|
|
45
|
+
recovering = false;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly agentName: string,
|
|
49
|
+
private readonly cm: ContextManager,
|
|
50
|
+
private readonly configured = false,
|
|
51
|
+
) {}
|
|
52
|
+
|
|
53
|
+
get enabled(): boolean { return this.override ?? this.configured; }
|
|
54
|
+
setOverride(value: boolean | undefined): void { this.override = value; }
|
|
55
|
+
get settingOverride(): boolean | undefined { return this.override; }
|
|
56
|
+
get hasPending(): boolean { return this.pending !== undefined; }
|
|
57
|
+
/** True only once the pending batch's originals were actually put on the
|
|
58
|
+
* wire. Guard effects (retry suppression, refusal claim, prose buffering)
|
|
59
|
+
* are scoped to this state; a merely staged batch never claims a refusal. */
|
|
60
|
+
get hasSubmittedPending(): boolean { return this.pending?.submitted === true; }
|
|
61
|
+
|
|
62
|
+
/** Extra input tokens the pending batch costs on the wire beyond the
|
|
63
|
+
* placeholder the strategy selected against (chars/4 + flat per image,
|
|
64
|
+
* the same heuristic as the physical-window projection). Compilation
|
|
65
|
+
* reserves this so substituting originals cannot exceed the budget. */
|
|
66
|
+
get pendingWireReserveTokens(): number {
|
|
67
|
+
const pending = this.pending;
|
|
68
|
+
if (!pending) return 0;
|
|
69
|
+
let chars = 0;
|
|
70
|
+
let images = 0;
|
|
71
|
+
for (const result of pending.wireResults) {
|
|
72
|
+
if (typeof result.content === 'string') chars += result.content.length;
|
|
73
|
+
else for (const block of result.content as ContentBlock[]) {
|
|
74
|
+
if (block.type === 'image') images += 1;
|
|
75
|
+
else chars += JSON.stringify(block).length;
|
|
76
|
+
}
|
|
77
|
+
chars -= TOOL_RESULT_GUARD_NOTICE.length;
|
|
78
|
+
}
|
|
79
|
+
return Math.max(0, Math.ceil(chars / 4) + images * 1600);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Ordered storage work, separate from batches awaiting a provider verdict.
|
|
83
|
+
* A failed acceptance edit retains both the payload and its compression
|
|
84
|
+
* hold here; it can never re-arm refusal handling or tool submission. */
|
|
85
|
+
private unrecorded: AuditOperation[] = [];
|
|
86
|
+
|
|
87
|
+
/** Retry edits and audit writes before a new activation, audit write,
|
|
88
|
+
* maintenance pass, or shutdown. A failed operation stays at the head. */
|
|
89
|
+
flushUnrecorded(): void {
|
|
90
|
+
while (this.unrecorded.length > 0) {
|
|
91
|
+
const operation = this.unrecorded[0];
|
|
92
|
+
try {
|
|
93
|
+
if (operation.admission) {
|
|
94
|
+
const { messageId, content, expected, branch } = operation.admission;
|
|
95
|
+
const sameBranch = this.cm.currentBranch().name === branch;
|
|
96
|
+
const current = sameBranch ? this.cm.getMessage(messageId) : null;
|
|
97
|
+
// A retry must not undo an operator's later edit/removal/rollback.
|
|
98
|
+
// Reapplying our own payload is safe if an earlier edit wrote the
|
|
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;
|
|
110
|
+
}
|
|
111
|
+
this.write(operation.record);
|
|
112
|
+
this.unrecorded.shift();
|
|
113
|
+
} finally {
|
|
114
|
+
if (!operation.admission) this.releaseOperationHold(operation);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private append(record: Record<string, unknown>): void {
|
|
120
|
+
const stamped = { agentName: this.agentName, timestamp: Date.now(), ...record };
|
|
121
|
+
this.unrecorded.push({ record: stamped });
|
|
122
|
+
this.flushUnrecorded();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private write(record: Record<string, unknown>): void {
|
|
126
|
+
const store = this.cm.getStore();
|
|
127
|
+
if (!this.registered) {
|
|
128
|
+
try {
|
|
129
|
+
store.registerState({ id: TOOL_RESULT_GUARD_AUDIT_STATE, strategy: 'append_log' });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!isStateExistsError(error)) throw error;
|
|
132
|
+
}
|
|
133
|
+
this.registered = true;
|
|
134
|
+
}
|
|
135
|
+
store.appendToStateJson(TOOL_RESULT_GUARD_AUDIT_STATE, record);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private archive(value: unknown): unknown {
|
|
139
|
+
const json = JSON.stringify(value);
|
|
140
|
+
// Match inference-log storage: large payloads (especially images and
|
|
141
|
+
// pre-spill output) must not be copied into every append-log snapshot.
|
|
142
|
+
return json.length > 10_000
|
|
143
|
+
? { blobId: this.cm.getStore().storeBlob(Buffer.from(json), 'application/json') }
|
|
144
|
+
: JSON.parse(json);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
storeResults(content: ContentBlock[], wireResults: ToolResult[], originals: CompletedToolCall[]): MessageId {
|
|
148
|
+
if (!this.enabled) return this.cm.addMessage('user', content);
|
|
149
|
+
if (this.pending) {
|
|
150
|
+
// Idempotent for the same not-yet-submitted batch: a caller that
|
|
151
|
+
// retries after a failure between staging and submission (e.g. a
|
|
152
|
+
// transient compile error on the direct Agent API) must not wedge.
|
|
153
|
+
const same = !this.pending.submitted
|
|
154
|
+
&& this.pending.wireResults.map((r) => r.toolUseId).join('\0')
|
|
155
|
+
=== wireResults.map((r) => r.toolUseId).join('\0');
|
|
156
|
+
if (same) return this.pending.messageId;
|
|
157
|
+
throw new Error('Tool result guard already has a pending batch');
|
|
158
|
+
}
|
|
159
|
+
const id = randomUUID();
|
|
160
|
+
// Includes full pre-truncation/error/image payloads, not just the wire
|
|
161
|
+
// preview. This slot is audit data, never a context/compression source.
|
|
162
|
+
const staged = { type: 'staged', batchId: id,
|
|
163
|
+
originals: this.archive(originals), content: this.archive(content), wireResults: this.archive(wireResults) };
|
|
164
|
+
try {
|
|
165
|
+
this.append(staged);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// Keep the original in the retry queue, but let the tool loop continue
|
|
168
|
+
// with a placeholder if the audit cannot become durable below.
|
|
169
|
+
console.error(`[tool-result-guard] agent=${this.agentName} staged audit write queued:`, error);
|
|
170
|
+
}
|
|
171
|
+
const withheld: ContentBlock[] = content.map((block) => block.type === 'tool_result'
|
|
172
|
+
? { type: 'tool_result', toolUseId: block.toolUseId, content: TOOL_RESULT_GUARD_NOTICE, isError: block.isError }
|
|
173
|
+
: block);
|
|
174
|
+
// Compression hold (CM >= 0.12): placed BEFORE onNewMessage fires, so no
|
|
175
|
+
// chunk is ever summarized from the placeholder; released after the
|
|
176
|
+
// settlement edit. Holds are in-memory only — a reopened manager has none,
|
|
177
|
+
// which is correct since a reopened guard has no pending batch.
|
|
178
|
+
const messageId = this.cm.addMessage('user', withheld, undefined, undefined, { holdCompression: true });
|
|
179
|
+
this.pending = { id, messageId, content, withheld, wireResults,
|
|
180
|
+
branch: this.cm.currentBranch().name, submitted: false, durable: false };
|
|
181
|
+
// Durability barrier: the audit must reach Chronicle's chain heads before
|
|
182
|
+
// the originals can go to a provider. On a failed sync the batch fails
|
|
183
|
+
// CLOSED: the placeholders go on the wire instead (the turn continues,
|
|
184
|
+
// nothing is stranded), and the batch later settles as unsubmitted.
|
|
185
|
+
try {
|
|
186
|
+
// A linked-append failure follows the same fail-closed path as a sync
|
|
187
|
+
// failure. Throwing here would strand the stream awaiting its results.
|
|
188
|
+
this.append({ type: 'linked', batchId: id, messageId });
|
|
189
|
+
this.cm.getStore().sync();
|
|
190
|
+
this.pending.durable = true;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error(`[tool-result-guard] agent=${this.agentName} audit persistence failed; ` +
|
|
193
|
+
'submitting placeholders instead of originals:', error);
|
|
194
|
+
}
|
|
195
|
+
return messageId;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Results for a live continuation (provideToolResults). Marks the batch
|
|
199
|
+
* submitted and returns the originals only when the audit is durable;
|
|
200
|
+
* otherwise returns placeholder results and leaves it unsubmitted. */
|
|
201
|
+
submissionResults(results: ToolResult[]): ToolResult[] {
|
|
202
|
+
const pending = this.pending;
|
|
203
|
+
if (!pending) return results;
|
|
204
|
+
if (pending.durable) { pending.submitted = true; return results; }
|
|
205
|
+
const ids = new Set(pending.wireResults.map((result) => result.toolUseId));
|
|
206
|
+
return results.map((result) => ids.has(result.toolUseId)
|
|
207
|
+
? { ...result, content: TOOL_RESULT_GUARD_NOTICE } : result);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The stream carrying this batch ended without a clean round (abort or
|
|
211
|
+
* exhausted errors). Settle conservatively — an interrupted submission
|
|
212
|
+
* does not establish acceptance — so a later turn neither resubmits the
|
|
213
|
+
* originals nor attributes its own refusal to this batch. */
|
|
214
|
+
abandon(reason: string): void {
|
|
215
|
+
const pending = this.pending;
|
|
216
|
+
this.recovering = false;
|
|
217
|
+
if (!pending) return;
|
|
218
|
+
this.settle(pending, { type: 'withheld', reason, submitted: pending.submitted }, false);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
/** A budget/error restart compiles placeholders; restore pending output
|
|
223
|
+
* only in this provider request, never in the strategy's view. */
|
|
224
|
+
prepareRequest(messages: NormalizedMessage[], recordSubmission = false): NormalizedMessage[] {
|
|
225
|
+
const pending = this.pending;
|
|
226
|
+
if (!pending || !pending.durable) return messages;
|
|
227
|
+
const byId = new Map(pending.wireResults.map((result) => [result.toolUseId, result]));
|
|
228
|
+
const present = new Set(messages.flatMap((message) => message.content
|
|
229
|
+
.filter((block) => block.type === 'tool_result' && byId.has(block.toolUseId))
|
|
230
|
+
.map((block) => (block as ContentBlock & { toolUseId: string }).toolUseId)));
|
|
231
|
+
// A strategy may have folded the entire exchange away. Do not release
|
|
232
|
+
// content that was never submitted. Its originals remain in the audit.
|
|
233
|
+
const submitted = present.size === byId.size;
|
|
234
|
+
if (recordSubmission) pending.submitted = submitted;
|
|
235
|
+
if (!submitted) return messages;
|
|
236
|
+
return messages.map((message) => ({ ...message, content: message.content.map((block) => {
|
|
237
|
+
const result = block.type === 'tool_result' ? byId.get(block.toolUseId) : undefined;
|
|
238
|
+
return result ? { type: 'tool_result', toolUseId: result.toolUseId, content: result.content, isError: result.isError } : block;
|
|
239
|
+
}) }));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The turn ended (endTurn/skip_reply) before the batch was submitted:
|
|
243
|
+
* nothing was refused, so admit it exactly as an unguarded agent would.
|
|
244
|
+
* Leaving it pending would turn it into a permanent withheld notice on
|
|
245
|
+
* restart and disable ordinary refusal handling on the next turn. */
|
|
246
|
+
settleTurnEnded(): void {
|
|
247
|
+
const pending = this.pending;
|
|
248
|
+
if (!pending) return;
|
|
249
|
+
this.settle(pending, pending.durable
|
|
250
|
+
? { type: 'accepted', reason: 'turn_ended' }
|
|
251
|
+
: { type: 'withheld', reason: 'audit_not_durable' }, pending.durable);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** A refusal arrived while the pending batch was never on the wire (the
|
|
255
|
+
* strategy omitted the exchange). The guard cannot have caused it: record
|
|
256
|
+
* the batch as withheld, clear it, and let ordinary refusal handling run.
|
|
257
|
+
* Returns true when it settled such a batch. */
|
|
258
|
+
settleUnsubmitted(category: string): boolean {
|
|
259
|
+
const pending = this.pending;
|
|
260
|
+
if (!pending || pending.submitted) return false;
|
|
261
|
+
this.settle(pending, { type: 'withheld', reason: 'unsubmitted', category }, false);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** A clean physical response accepts precisely the last submitted batch. */
|
|
266
|
+
accept(): void {
|
|
267
|
+
const pending = this.pending;
|
|
268
|
+
this.recovering = false;
|
|
269
|
+
if (!pending) return;
|
|
270
|
+
this.settle(pending, pending.submitted
|
|
271
|
+
? { type: 'accepted' } : { type: 'withheld', reason: 'unsubmitted' }, pending.submitted);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** At most one recovery per batch; no scanning/deleting older history. */
|
|
275
|
+
withhold(category: string): string[] | null {
|
|
276
|
+
const pending = this.pending;
|
|
277
|
+
if (!pending?.submitted) return null;
|
|
278
|
+
const ids = pending.wireResults.map((result) => result.toolUseId);
|
|
279
|
+
// Even a failed outcome-log write must never re-arm rejected output for
|
|
280
|
+
// a later submission. Its originals were archived before admission.
|
|
281
|
+
this.recovering = true;
|
|
282
|
+
this.settle(pending, { type: 'withheld', toolUseIds: ids, category }, false);
|
|
283
|
+
return ids;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Transfer ownership from provider admission to an ordered storage job.
|
|
287
|
+
* Failure retains the accepted payload and its hold until the edit succeeds.
|
|
288
|
+
* Withheld content is final already, so audit failures cannot keep it held. */
|
|
289
|
+
private settle(pending: PendingBatch, record: Record<string, unknown>, admit: boolean): void {
|
|
290
|
+
this.pending = undefined;
|
|
291
|
+
const operation: AuditOperation = {
|
|
292
|
+
record: { agentName: this.agentName, timestamp: Date.now(), ...record,
|
|
293
|
+
batchId: pending.id, messageId: pending.messageId, sourceBranch: pending.branch },
|
|
294
|
+
...(admit ? { admission: { messageId: pending.messageId, content: pending.content,
|
|
295
|
+
expected: pending.withheld, branch: pending.branch } } : {}),
|
|
296
|
+
heldMessageId: pending.messageId,
|
|
297
|
+
};
|
|
298
|
+
this.unrecorded.push(operation);
|
|
299
|
+
try {
|
|
300
|
+
this.flushUnrecorded();
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error(`[tool-result-guard] agent=${this.agentName} settlement queued for retry:`, error);
|
|
303
|
+
} finally {
|
|
304
|
+
if (!operation.admission) this.releaseOperationHold(operation);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private releaseOperationHold(operation: AuditOperation): void {
|
|
309
|
+
if (operation.heldMessageId !== undefined) {
|
|
310
|
+
this.releaseHold(operation.heldMessageId);
|
|
311
|
+
operation.heldMessageId = undefined;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private releaseHold(messageId: MessageId): void {
|
|
316
|
+
this.cm.releaseCompression([messageId]);
|
|
317
|
+
}
|
|
318
|
+
}
|
package/src/types/agent.ts
CHANGED
|
@@ -170,6 +170,12 @@ export interface AgentConfig {
|
|
|
170
170
|
announceHumanTurns?: boolean;
|
|
171
171
|
};
|
|
172
172
|
|
|
173
|
+
/** Opt-in tool-output admission and one guarded retry after a provider
|
|
174
|
+
* refusal. Agents can persistently override this via agent_settings
|
|
175
|
+
* tool_result_guard. Full originals remain in Chronicle's audit history.
|
|
176
|
+
* Default false. Disabling does not restore previously withheld output. */
|
|
177
|
+
toolResultGuard?: boolean;
|
|
178
|
+
|
|
173
179
|
/**
|
|
174
180
|
* How the agent's PLAIN PROSE (non-tool output) reaches channels.
|
|
175
181
|
* - 'locus' (default): host-inferred — the turn-frozen locus machinery.
|
|
@@ -296,6 +302,9 @@ export interface CompletedToolCall {
|
|
|
296
302
|
* Inference request for an agent.
|
|
297
303
|
*/
|
|
298
304
|
export interface InferenceRequest {
|
|
305
|
+
/** Host-owned identity for withdrawal of an unconsumed coalesced wake. */
|
|
306
|
+
coalescingSubject?: string;
|
|
307
|
+
coalescingEventId?: string;
|
|
299
308
|
agentName: string;
|
|
300
309
|
reason: string;
|
|
301
310
|
source: string;
|
|
@@ -336,4 +345,10 @@ export interface InferenceRequest {
|
|
|
336
345
|
wakeChannelId?: string;
|
|
337
346
|
/** Timestamp of the event the wake provenance was taken from (ms). */
|
|
338
347
|
wakeAt?: number;
|
|
348
|
+
/** Suppress every automatic plain-prose delivery for this logical turn.
|
|
349
|
+
* Explicit tool calls remain available. Used by authenticated silent wakes. */
|
|
350
|
+
suppressProse?: boolean;
|
|
351
|
+
/** Ephemeral system-position prompt for this turn only. Never written to
|
|
352
|
+
* Chronicle; callers must supply bounded non-secret control text. */
|
|
353
|
+
ephemeralSystemPrompt?: string;
|
|
339
354
|
}
|
package/src/types/framework.ts
CHANGED
|
@@ -52,6 +52,12 @@ export interface CodeExecutionConfig {
|
|
|
52
52
|
toolCallTimeoutMs?: number;
|
|
53
53
|
/** Whole-script deadline: cancel → grace → SIGKILL (default 600_000 ms). */
|
|
54
54
|
scriptTimeoutMs?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Longest deadline an agent may ask for on one call with `time_limit_ms`
|
|
57
|
+
* (default: `scriptTimeoutMs`, so agents can only shorten it until this is
|
|
58
|
+
* raised). Longer requests are capped, and the result says so.
|
|
59
|
+
*/
|
|
60
|
+
maxScriptTimeoutMs?: number;
|
|
55
61
|
/**
|
|
56
62
|
* Idle interpreter reclaim — script globals are lost after this much
|
|
57
63
|
* inactivity (default 300_000 ms, mirroring ~5-minute container reclaim).
|
|
@@ -225,6 +231,23 @@ export interface FrameworkConfig {
|
|
|
225
231
|
*/
|
|
226
232
|
toolResultInlineMaxChars?: number;
|
|
227
233
|
|
|
234
|
+
/**
|
|
235
|
+
* MCPL RFC-008 operator class overrides: RFC-007 §6.2 patterns over the
|
|
236
|
+
* model-facing tool name → classes. Highest-precedence source of a tool's
|
|
237
|
+
* effective class (first matching pattern wins); replaces, never merges
|
|
238
|
+
* with, what the providing server declared. Use it to correct or tighten a
|
|
239
|
+
* misclassed tool.
|
|
240
|
+
*/
|
|
241
|
+
toolClassOverrides?: Record<string, string[]>;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* MCPL RFC-008 host knowledge for tools the EMBEDDING host implements (its
|
|
245
|
+
* own modules), same shape as toolClassOverrides. Consulted before the
|
|
246
|
+
* framework's built-in table and never for MCPL-provided tools, whose
|
|
247
|
+
* class comes from an override or their server's `_meta["mcpl/class"]`.
|
|
248
|
+
*/
|
|
249
|
+
hostToolClasses?: Record<string, string[]>;
|
|
250
|
+
|
|
228
251
|
/** Inference routing policy for server-initiated inference (optional). */
|
|
229
252
|
inferenceRouting?: InferenceRoutingPolicy;
|
|
230
253
|
|
package/src/types/trace.ts
CHANGED
|
@@ -48,6 +48,8 @@ export type TraceEvent =
|
|
|
48
48
|
* activation with its channel without re-deriving routing.
|
|
49
49
|
*/
|
|
50
50
|
channelId?: string;
|
|
51
|
+
/** True for a control-plane wake that must not surface typing/prose. */
|
|
52
|
+
silent?: boolean;
|
|
51
53
|
})
|
|
52
54
|
| (TraceEventBase & {
|
|
53
55
|
type: 'inference:completed';
|
|
@@ -209,6 +211,21 @@ export type TraceEvent =
|
|
|
209
211
|
messageId: string;
|
|
210
212
|
source: string;
|
|
211
213
|
})
|
|
214
|
+
/** An unread message withdrawn or replaced by its sender (RFC-006). */
|
|
215
|
+
| (TraceEventBase & {
|
|
216
|
+
type: 'message:removed';
|
|
217
|
+
messageId: string;
|
|
218
|
+
source: string;
|
|
219
|
+
})
|
|
220
|
+
/** RFC-006 coalescing decision record (received / displaced / removed /
|
|
221
|
+
* rendered / revoked / render-cancelled / late-render / render-failed). */
|
|
222
|
+
| (TraceEventBase & {
|
|
223
|
+
type: 'mcpl:coalescing';
|
|
224
|
+
kind: string;
|
|
225
|
+
subject?: string;
|
|
226
|
+
eventId?: string;
|
|
227
|
+
[key: string]: unknown;
|
|
228
|
+
})
|
|
212
229
|
|
|
213
230
|
// EventGate lifecycle
|
|
214
231
|
| (TraceEventBase & {
|