@animalabs/agent-framework 0.6.5 → 0.6.7
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 +78 -0
- package/dist/src/agent.d.ts +56 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +372 -57
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server.js +0 -0
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +7 -2
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/canonical-json.d.ts +2 -0
- package/dist/src/canonical-json.d.ts.map +1 -0
- package/dist/src/canonical-json.js +23 -0
- package/dist/src/canonical-json.js.map +1 -0
- package/dist/src/framework.d.ts +160 -2
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3098 -167
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +34 -4
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +36 -15
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +322 -76
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +1 -0
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +1 -0
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +39 -4
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +146 -28
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +43 -12
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js +1 -0
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +12 -1
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +661 -2
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +4 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts +99 -0
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -0
- package/dist/src/recovery/discord-awareness-outbox.js +334 -0
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -0
- package/dist/src/recovery/offline-branch.d.ts +45 -0
- package/dist/src/recovery/offline-branch.d.ts.map +1 -0
- package/dist/src/recovery/offline-branch.js +362 -0
- package/dist/src/recovery/offline-branch.js.map +1 -0
- package/dist/src/recovery/recover-cli.d.ts +3 -0
- package/dist/src/recovery/recover-cli.d.ts.map +1 -0
- package/dist/src/recovery/recover-cli.js +129 -0
- package/dist/src/recovery/recover-cli.js.map +1 -0
- package/dist/src/types/agent.d.ts +37 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +1 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +14 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +31 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +4 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/agent-runtime-settings.test.d.ts +2 -0
- package/dist/test/agent-runtime-settings.test.d.ts.map +1 -0
- package/dist/test/agent-runtime-settings.test.js +227 -0
- package/dist/test/agent-runtime-settings.test.js.map +1 -0
- package/dist/test/busy-agent-scheduler.test.d.ts +2 -0
- package/dist/test/busy-agent-scheduler.test.d.ts.map +1 -0
- package/dist/test/busy-agent-scheduler.test.js +62 -0
- package/dist/test/busy-agent-scheduler.test.js.map +1 -0
- package/dist/test/channel-lifecycle-provenance.test.d.ts +2 -0
- package/dist/test/channel-lifecycle-provenance.test.d.ts.map +1 -0
- package/dist/test/channel-lifecycle-provenance.test.js +47 -0
- package/dist/test/channel-lifecycle-provenance.test.js.map +1 -0
- package/dist/test/channel-registry-routing.test.js +5 -4
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/conversation-fork-injection-scoping.test.js +12 -1
- package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
- package/dist/test/discord-awareness-outbox.test.d.ts +2 -0
- package/dist/test/discord-awareness-outbox.test.d.ts.map +1 -0
- package/dist/test/discord-awareness-outbox.test.js +248 -0
- package/dist/test/discord-awareness-outbox.test.js.map +1 -0
- package/dist/test/event-gate.test.js +21 -1
- package/dist/test/event-gate.test.js.map +1 -1
- package/dist/test/helpers/mock-membrane.d.ts +13 -1
- package/dist/test/helpers/mock-membrane.d.ts.map +1 -1
- package/dist/test/helpers/mock-membrane.js +9 -2
- package/dist/test/helpers/mock-membrane.js.map +1 -1
- package/dist/test/maintenance-timer.test.js +103 -1
- package/dist/test/maintenance-timer.test.js.map +1 -1
- package/dist/test/mcpl-awareness-barrier.test.d.ts +2 -0
- package/dist/test/mcpl-awareness-barrier.test.d.ts.map +1 -0
- package/dist/test/mcpl-awareness-barrier.test.js +845 -0
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -0
- package/dist/test/mcpl-gate-roundtrip.test.js +138 -6
- package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
- package/dist/test/mcpl-reconnect-reregistration.test.js +22 -3
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
- package/dist/test/mcpl-request-timeout.test.js +15 -0
- package/dist/test/mcpl-request-timeout.test.js.map +1 -1
- package/dist/test/offline-recovery-branch.test.d.ts +2 -0
- package/dist/test/offline-recovery-branch.test.d.ts.map +1 -0
- package/dist/test/offline-recovery-branch.test.js +226 -0
- package/dist/test/offline-recovery-branch.test.js.map +1 -0
- package/dist/test/present-while-acting.test.d.ts +19 -0
- package/dist/test/present-while-acting.test.d.ts.map +1 -0
- package/dist/test/present-while-acting.test.js +295 -0
- package/dist/test/present-while-acting.test.js.map +1 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +2 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +1 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.js +1955 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +1 -0
- package/dist/test/primary-summary-refusal-fallback.test.d.ts +2 -0
- package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +1 -0
- package/dist/test/primary-summary-refusal-fallback.test.js +753 -0
- package/dist/test/primary-summary-refusal-fallback.test.js.map +1 -0
- package/dist/test/same-round-think-policy.test.d.ts +2 -0
- package/dist/test/same-round-think-policy.test.d.ts.map +1 -0
- package/dist/test/same-round-think-policy.test.js +853 -0
- package/dist/test/same-round-think-policy.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +38 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -1
- package/dist/test/workspace-read-image.test.d.ts +2 -0
- package/dist/test/workspace-read-image.test.d.ts.map +1 -0
- package/dist/test/workspace-read-image.test.js +518 -0
- package/dist/test/workspace-read-image.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -4
- package/src/agent.ts +485 -63
- package/src/api/server.ts +8 -2
- package/src/canonical-json.ts +22 -0
- package/src/framework.ts +4096 -304
- package/src/gate/event-gate.ts +50 -4
- package/src/index.ts +20 -0
- package/src/mcpl/channel-registry.ts +396 -85
- package/src/mcpl/push-handler.ts +2 -0
- package/src/mcpl/server-connection.ts +173 -28
- package/src/mcpl/types.ts +49 -12
- package/src/modules/workspace/index.ts +759 -2
- package/src/modules/workspace/types.ts +5 -0
- package/src/recovery/discord-awareness-outbox.ts +459 -0
- package/src/recovery/offline-branch.ts +510 -0
- package/src/recovery/recover-cli.ts +130 -0
- package/src/types/agent.ts +44 -0
- package/src/types/events.ts +1 -1
- package/src/types/framework.ts +17 -0
- package/src/types/module.ts +33 -0
- package/src/types/trace.ts +4 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { JsStore } from '@animalabs/chronicle';
|
|
2
|
+
import { ContextManager } from '@animalabs/context-manager';
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_DISCORD_AWARENESS_EMOJI,
|
|
5
|
+
DiscordAwarenessOutbox,
|
|
6
|
+
defaultDiscordAwarenessOutboxPath,
|
|
7
|
+
extractDiscordAwarenessRefs,
|
|
8
|
+
type DiscordAwarenessRef,
|
|
9
|
+
} from './discord-awareness-outbox.js';
|
|
10
|
+
|
|
11
|
+
export interface OfflineRecoveryBranchOptions {
|
|
12
|
+
storePath: string;
|
|
13
|
+
agentName: string;
|
|
14
|
+
/** Preferred: Discord message that remains as the last visible anchor. */
|
|
15
|
+
messageId?: string;
|
|
16
|
+
/** Exact internal ContextManager message ID that remains as the anchor. */
|
|
17
|
+
contextId?: string;
|
|
18
|
+
/** Backward-compatible alternative: remove the newest N context entries. */
|
|
19
|
+
messages?: number;
|
|
20
|
+
/** Exact Discord messages to suppress on the new branch. */
|
|
21
|
+
suppressMessageIds?: string[];
|
|
22
|
+
/** Inclusive context-order ranges between Discord message endpoints. */
|
|
23
|
+
suppressRanges?: Array<{ fromMessageId: string; toMessageId: string }>;
|
|
24
|
+
branchName?: string;
|
|
25
|
+
/** Defaults to the primary-agent namespace `agents/{agentName}`. */
|
|
26
|
+
namespace?: string;
|
|
27
|
+
/** Use for old message records that do not carry metadata.serverId. */
|
|
28
|
+
discordServerId?: string;
|
|
29
|
+
outboxPath?: string;
|
|
30
|
+
emoji?: string;
|
|
31
|
+
dryRun?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface OfflineRecoveryBranchResult {
|
|
35
|
+
dryRun: boolean;
|
|
36
|
+
sourceBranch: string;
|
|
37
|
+
targetBranch: string;
|
|
38
|
+
messagesRemoved: number;
|
|
39
|
+
messagesSuppressed: number;
|
|
40
|
+
discordMarkersQueued: number;
|
|
41
|
+
refs: DiscordAwarenessRef[];
|
|
42
|
+
outboxPath: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a safe Chronicle branch while the normal agent host is stopped.
|
|
47
|
+
*
|
|
48
|
+
* History is scanned in bounded windows and blobs are not re-inlined. The
|
|
49
|
+
* affected messages are never compiled and their content is never written to
|
|
50
|
+
* the recovery outbox; only Discord addressing metadata leaves Chronicle.
|
|
51
|
+
*/
|
|
52
|
+
export async function createOfflineRecoveryBranch(
|
|
53
|
+
options: OfflineRecoveryBranchOptions,
|
|
54
|
+
): Promise<OfflineRecoveryBranchResult> {
|
|
55
|
+
const anchorCount = Number(!!options.messageId)
|
|
56
|
+
+ Number(!!options.contextId)
|
|
57
|
+
+ Number(options.messages !== undefined);
|
|
58
|
+
if (anchorCount !== 1) {
|
|
59
|
+
throw new Error('Specify exactly one of messageId, contextId, or messages');
|
|
60
|
+
}
|
|
61
|
+
const count = options.messages === undefined ? undefined : Math.floor(options.messages);
|
|
62
|
+
if (count !== undefined && (!Number.isFinite(count) || count < 1)) {
|
|
63
|
+
throw new Error('messages must be a positive integer');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const store = JsStore.openOrCreate({ path: options.storePath });
|
|
67
|
+
let contextManager: ContextManager | null = null;
|
|
68
|
+
try {
|
|
69
|
+
contextManager = await ContextManager.open({
|
|
70
|
+
store,
|
|
71
|
+
namespace: options.namespace ?? `agents/${options.agentName}`,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const total = contextManager.getMessageCount();
|
|
75
|
+
if (count !== undefined && count >= total) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Cannot remove ${count} message(s) — history has ${total}; at least one must remain.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let target: StoredWindowMessage;
|
|
82
|
+
let targetIndex: number;
|
|
83
|
+
let removedCount: number;
|
|
84
|
+
let refs: DiscordAwarenessRef[];
|
|
85
|
+
|
|
86
|
+
if (options.messageId) {
|
|
87
|
+
// Search backward in bounded windows: recovery anchors are normally near
|
|
88
|
+
// the tail, and even a very large Chronicle is never materialized all at
|
|
89
|
+
// once. If an adaptive strategy sharded the Discord message, choosing the
|
|
90
|
+
// newest matching shard keeps the complete message at the branch point.
|
|
91
|
+
const anchorId = normalizeDiscordMessageId(options.messageId);
|
|
92
|
+
const located = findDiscordMessageFromTail(contextManager, total, anchorId);
|
|
93
|
+
if (!located) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Discord message ${anchorId} is not an addressable message in this context.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
target = located.message;
|
|
99
|
+
targetIndex = located.index;
|
|
100
|
+
removedCount = total - located.index - 1;
|
|
101
|
+
refs = collectDiscordRefs(
|
|
102
|
+
contextManager,
|
|
103
|
+
located.index + 1,
|
|
104
|
+
total,
|
|
105
|
+
options.discordServerId,
|
|
106
|
+
);
|
|
107
|
+
} else if (options.contextId) {
|
|
108
|
+
const located = findInternalMessageFromTail(contextManager, total, options.contextId.trim());
|
|
109
|
+
if (!located) {
|
|
110
|
+
throw new Error(`Internal context message ${options.contextId} was not found.`);
|
|
111
|
+
}
|
|
112
|
+
target = located.message;
|
|
113
|
+
targetIndex = located.index;
|
|
114
|
+
removedCount = total - located.index - 1;
|
|
115
|
+
refs = collectDiscordRefs(
|
|
116
|
+
contextManager,
|
|
117
|
+
located.index + 1,
|
|
118
|
+
total,
|
|
119
|
+
options.discordServerId,
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
// Count mode remains for compatibility. Read only the target plus suffix,
|
|
123
|
+
// avoiding full-history materialization and attachment blob inflation.
|
|
124
|
+
const window = contextManager.getMessageWindow(total - count! - 1, count! + 1, {
|
|
125
|
+
resolveBlobs: false,
|
|
126
|
+
}).messages;
|
|
127
|
+
const countTarget = window[0];
|
|
128
|
+
const discarded = window.slice(1);
|
|
129
|
+
if (!countTarget || discarded.length !== count) {
|
|
130
|
+
throw new Error('Could not read the requested recovery window from Chronicle');
|
|
131
|
+
}
|
|
132
|
+
target = countTarget;
|
|
133
|
+
targetIndex = total - count! - 1;
|
|
134
|
+
removedCount = count!;
|
|
135
|
+
refs = extractDiscordAwarenessRefs(discarded, options.discordServerId);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
validateAnchorBoundary(contextManager, targetIndex, total, target);
|
|
139
|
+
|
|
140
|
+
const suppression = buildSuppressionPlan(
|
|
141
|
+
contextManager,
|
|
142
|
+
targetIndex,
|
|
143
|
+
options.suppressMessageIds ?? [],
|
|
144
|
+
options.suppressRanges ?? [],
|
|
145
|
+
options.discordServerId,
|
|
146
|
+
);
|
|
147
|
+
validateRemovalIntegrity(
|
|
148
|
+
contextManager,
|
|
149
|
+
total,
|
|
150
|
+
[
|
|
151
|
+
...(removedCount > 0 ? [{
|
|
152
|
+
start: targetIndex + 1,
|
|
153
|
+
end: total - 1,
|
|
154
|
+
fromId: '',
|
|
155
|
+
toId: '',
|
|
156
|
+
}] : []),
|
|
157
|
+
...suppression.intervals,
|
|
158
|
+
],
|
|
159
|
+
);
|
|
160
|
+
refs = dedupeDiscordRefs([...refs, ...suppression.refs]);
|
|
161
|
+
|
|
162
|
+
const sourceBranch = store.currentBranch().name;
|
|
163
|
+
const targetBranch = options.branchName
|
|
164
|
+
?? `recovery/${options.agentName}/${Date.now()}`;
|
|
165
|
+
if (targetBranch === sourceBranch) {
|
|
166
|
+
throw new Error('Recovery branch name must differ from the active source branch');
|
|
167
|
+
}
|
|
168
|
+
const outboxPath = options.outboxPath
|
|
169
|
+
?? defaultDiscordAwarenessOutboxPath(options.storePath);
|
|
170
|
+
|
|
171
|
+
const result: OfflineRecoveryBranchResult = {
|
|
172
|
+
dryRun: options.dryRun === true,
|
|
173
|
+
sourceBranch,
|
|
174
|
+
targetBranch,
|
|
175
|
+
messagesRemoved: removedCount,
|
|
176
|
+
messagesSuppressed: suppression.messageCount,
|
|
177
|
+
discordMarkersQueued: refs.length,
|
|
178
|
+
refs,
|
|
179
|
+
outboxPath,
|
|
180
|
+
};
|
|
181
|
+
if (options.dryRun) return result;
|
|
182
|
+
|
|
183
|
+
const outbox = new DiscordAwarenessOutbox(outboxPath);
|
|
184
|
+
const batch = outbox.prepare({
|
|
185
|
+
agentName: options.agentName,
|
|
186
|
+
sourceBranch,
|
|
187
|
+
targetBranch,
|
|
188
|
+
refs,
|
|
189
|
+
emoji: options.emoji ?? DEFAULT_DISCORD_AWARENESS_EMOJI,
|
|
190
|
+
// Merely seeing targetBranch active does not prove branch-local
|
|
191
|
+
// suppressions finished. Only the recovery operation may activate this
|
|
192
|
+
// batch after every removal below succeeds.
|
|
193
|
+
activationPolicy: suppression.intervals.length > 0 ? 'explicit' : 'target-branch',
|
|
194
|
+
suppressionIntervals: suppression.intervals.map((interval) => ({
|
|
195
|
+
fromId: String(interval.fromId),
|
|
196
|
+
toId: String(interval.toId),
|
|
197
|
+
})),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// branchAt uses the target message's origin sequence. No message content is
|
|
201
|
+
// compiled or submitted to Membrane during this operation.
|
|
202
|
+
const createdBranch = contextManager.branchAt(target.id, targetBranch);
|
|
203
|
+
try {
|
|
204
|
+
await contextManager.switchBranch(createdBranch);
|
|
205
|
+
// Work from newest to oldest so earlier redactions never perturb the
|
|
206
|
+
// live positions used by later range endpoints.
|
|
207
|
+
for (const interval of [...suppression.intervals].reverse()) {
|
|
208
|
+
if (interval.fromId === interval.toId) contextManager.removeMessage(interval.fromId);
|
|
209
|
+
else contextManager.removeMessages(interval.fromId, interval.toId);
|
|
210
|
+
}
|
|
211
|
+
if (batch) outbox.activate(batch.id);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
// A partially suppressed branch is not safe to boot into. Preserve it
|
|
214
|
+
// for diagnosis, but leave Chronicle on the untouched source branch and
|
|
215
|
+
// keep the explicit outbox batch non-deliverable.
|
|
216
|
+
if (store.currentBranch().name === createdBranch) {
|
|
217
|
+
await contextManager.switchBranch(sourceBranch);
|
|
218
|
+
}
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return result;
|
|
223
|
+
} finally {
|
|
224
|
+
contextManager?.close();
|
|
225
|
+
store.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const RECOVERY_SCAN_WINDOW = 1_000;
|
|
230
|
+
type StoredWindowMessage = ReturnType<ContextManager['getMessageWindow']>['messages'][number];
|
|
231
|
+
|
|
232
|
+
interface SuppressionInterval {
|
|
233
|
+
start: number;
|
|
234
|
+
end: number;
|
|
235
|
+
fromId: StoredWindowMessage['id'];
|
|
236
|
+
toId: StoredWindowMessage['id'];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface MessageLocation {
|
|
240
|
+
first: number;
|
|
241
|
+
last: number;
|
|
242
|
+
firstId: StoredWindowMessage['id'];
|
|
243
|
+
lastId: StoredWindowMessage['id'];
|
|
244
|
+
/** Usually one run; multiple runs tolerate duplicate external metadata. */
|
|
245
|
+
runs: SuppressionInterval[];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeDiscordMessageId(input: string): string {
|
|
249
|
+
const trimmed = input.trim();
|
|
250
|
+
const link = trimmed.match(/channels\/\d+\/\d+\/(\d+)/);
|
|
251
|
+
return link?.[1] ?? trimmed;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function findDiscordMessageFromTail(
|
|
255
|
+
contextManager: ContextManager,
|
|
256
|
+
total: number,
|
|
257
|
+
messageId: string,
|
|
258
|
+
): { message: ReturnType<ContextManager['getMessageWindow']>['messages'][number]; index: number } | null {
|
|
259
|
+
let end = total;
|
|
260
|
+
while (end > 0) {
|
|
261
|
+
const start = Math.max(0, end - RECOVERY_SCAN_WINDOW);
|
|
262
|
+
const messages = contextManager.getMessageWindow(start, end - start, {
|
|
263
|
+
resolveBlobs: false,
|
|
264
|
+
}).messages;
|
|
265
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
266
|
+
if (String(messages[i].metadata?.messageId ?? '') === messageId) {
|
|
267
|
+
return { message: messages[i], index: start + i };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
end = start;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function findInternalMessageFromTail(
|
|
276
|
+
contextManager: ContextManager,
|
|
277
|
+
total: number,
|
|
278
|
+
contextId: string,
|
|
279
|
+
): { message: StoredWindowMessage; index: number } | null {
|
|
280
|
+
let end = total;
|
|
281
|
+
while (end > 0) {
|
|
282
|
+
const start = Math.max(0, end - RECOVERY_SCAN_WINDOW);
|
|
283
|
+
const messages = contextManager.getMessageWindow(start, end - start, {
|
|
284
|
+
resolveBlobs: false,
|
|
285
|
+
}).messages;
|
|
286
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
287
|
+
if (String(messages[i].id) === contextId) {
|
|
288
|
+
return { message: messages[i], index: start + i };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
end = start;
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function validateAnchorBoundary(
|
|
297
|
+
contextManager: ContextManager,
|
|
298
|
+
targetIndex: number,
|
|
299
|
+
total: number,
|
|
300
|
+
target: StoredWindowMessage,
|
|
301
|
+
): void {
|
|
302
|
+
const groupId = (target as StoredWindowMessage & { bodyGroupId?: string }).bodyGroupId;
|
|
303
|
+
if (!groupId || targetIndex + 1 >= total) return;
|
|
304
|
+
const next = contextManager.getMessageWindow(targetIndex + 1, 1, {
|
|
305
|
+
resolveBlobs: false,
|
|
306
|
+
}).messages[0] as (StoredWindowMessage & { bodyGroupId?: string }) | undefined;
|
|
307
|
+
if (next?.bodyGroupId === groupId) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Recovery anchor ${String(target.id)} splits body group ${groupId}; choose its final shard.`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function validateRemovalIntegrity(
|
|
315
|
+
contextManager: ContextManager,
|
|
316
|
+
total: number,
|
|
317
|
+
intervals: SuppressionInterval[],
|
|
318
|
+
): void {
|
|
319
|
+
if (intervals.length === 0) return;
|
|
320
|
+
const removed = (index: number) => intervals.some((interval) =>
|
|
321
|
+
index >= interval.start && index <= interval.end);
|
|
322
|
+
const toolUses = new Map<string, boolean>();
|
|
323
|
+
const toolResults = new Map<string, boolean>();
|
|
324
|
+
const groupMembership = new Map<string, boolean>();
|
|
325
|
+
|
|
326
|
+
for (let offset = 0; offset < total; offset += RECOVERY_SCAN_WINDOW) {
|
|
327
|
+
const messages = contextManager.getMessageWindow(
|
|
328
|
+
offset,
|
|
329
|
+
Math.min(RECOVERY_SCAN_WINDOW, total - offset),
|
|
330
|
+
{ resolveBlobs: false },
|
|
331
|
+
).messages;
|
|
332
|
+
for (let i = 0; i < messages.length; i++) {
|
|
333
|
+
const index = offset + i;
|
|
334
|
+
const isRemoved = removed(index);
|
|
335
|
+
const message = messages[i] as StoredWindowMessage & { bodyGroupId?: string };
|
|
336
|
+
if (message.bodyGroupId) {
|
|
337
|
+
const prior = groupMembership.get(message.bodyGroupId);
|
|
338
|
+
if (prior !== undefined && prior !== isRemoved) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`Recovery selection splits body group ${message.bodyGroupId}; adjust the anchor or suppression boundary.`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
groupMembership.set(message.bodyGroupId, isRemoved);
|
|
344
|
+
}
|
|
345
|
+
for (const rawBlock of message.content) {
|
|
346
|
+
const block = rawBlock as { type?: unknown; id?: unknown; toolUseId?: unknown };
|
|
347
|
+
if (block.type === 'tool_use' && typeof block.id === 'string') {
|
|
348
|
+
toolUses.set(block.id, isRemoved);
|
|
349
|
+
} else if (block.type === 'tool_result' && typeof block.toolUseId === 'string') {
|
|
350
|
+
toolResults.set(block.toolUseId, isRemoved);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (const [toolUseId, useRemoved] of toolUses) {
|
|
357
|
+
const resultRemoved = toolResults.get(toolUseId);
|
|
358
|
+
if (resultRemoved !== undefined && resultRemoved !== useRemoved) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`Recovery selection would split tool exchange ${toolUseId}; suppress or retain both tool_use and tool_result.`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function buildSuppressionPlan(
|
|
367
|
+
contextManager: ContextManager,
|
|
368
|
+
targetIndex: number,
|
|
369
|
+
rawMessageIds: string[],
|
|
370
|
+
rawRanges: Array<{ fromMessageId: string; toMessageId: string }>,
|
|
371
|
+
forcedServerId?: string,
|
|
372
|
+
): { intervals: SuppressionInterval[]; refs: DiscordAwarenessRef[]; messageCount: number } {
|
|
373
|
+
const messageIds = rawMessageIds.map(normalizeDiscordMessageId);
|
|
374
|
+
const ranges = rawRanges.map((range) => ({
|
|
375
|
+
fromMessageId: normalizeDiscordMessageId(range.fromMessageId),
|
|
376
|
+
toMessageId: normalizeDiscordMessageId(range.toMessageId),
|
|
377
|
+
}));
|
|
378
|
+
const wanted = new Set([
|
|
379
|
+
...messageIds,
|
|
380
|
+
...ranges.flatMap((range) => [range.fromMessageId, range.toMessageId]),
|
|
381
|
+
]);
|
|
382
|
+
if (wanted.size === 0) return { intervals: [], refs: [], messageCount: 0 };
|
|
383
|
+
|
|
384
|
+
const locations = new Map<string, MessageLocation>();
|
|
385
|
+
for (let offset = 0; offset <= targetIndex; offset += RECOVERY_SCAN_WINDOW) {
|
|
386
|
+
const messages = contextManager.getMessageWindow(
|
|
387
|
+
offset,
|
|
388
|
+
Math.min(RECOVERY_SCAN_WINDOW, targetIndex - offset + 1),
|
|
389
|
+
{ resolveBlobs: false },
|
|
390
|
+
).messages;
|
|
391
|
+
for (let i = 0; i < messages.length; i++) {
|
|
392
|
+
const externalId = String(messages[i].metadata?.messageId ?? '');
|
|
393
|
+
if (!wanted.has(externalId)) continue;
|
|
394
|
+
const index = offset + i;
|
|
395
|
+
const location = locations.get(externalId);
|
|
396
|
+
if (!location) {
|
|
397
|
+
const run = {
|
|
398
|
+
start: index,
|
|
399
|
+
end: index,
|
|
400
|
+
fromId: messages[i].id,
|
|
401
|
+
toId: messages[i].id,
|
|
402
|
+
};
|
|
403
|
+
locations.set(externalId, {
|
|
404
|
+
first: index,
|
|
405
|
+
last: index,
|
|
406
|
+
firstId: messages[i].id,
|
|
407
|
+
lastId: messages[i].id,
|
|
408
|
+
runs: [run],
|
|
409
|
+
});
|
|
410
|
+
} else {
|
|
411
|
+
const lastRun = location.runs.at(-1)!;
|
|
412
|
+
if (index === lastRun.end + 1) {
|
|
413
|
+
lastRun.end = index;
|
|
414
|
+
lastRun.toId = messages[i].id;
|
|
415
|
+
} else {
|
|
416
|
+
location.runs.push({
|
|
417
|
+
start: index,
|
|
418
|
+
end: index,
|
|
419
|
+
fromId: messages[i].id,
|
|
420
|
+
toId: messages[i].id,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
location.last = index;
|
|
424
|
+
location.lastId = messages[i].id;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (const id of wanted) {
|
|
430
|
+
if (!locations.has(id)) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Suppression endpoint ${id} is not visible at the selected branch point.`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const intervals: SuppressionInterval[] = [];
|
|
438
|
+
for (const id of messageIds) {
|
|
439
|
+
const location = locations.get(id)!;
|
|
440
|
+
intervals.push(...location.runs.map((run) => ({ ...run })));
|
|
441
|
+
}
|
|
442
|
+
for (const range of ranges) {
|
|
443
|
+
const from = locations.get(range.fromMessageId)!;
|
|
444
|
+
const to = locations.get(range.toMessageId)!;
|
|
445
|
+
const earlier = from.first <= to.first ? from : to;
|
|
446
|
+
const later = from.first <= to.first ? to : from;
|
|
447
|
+
intervals.push({
|
|
448
|
+
start: earlier.first,
|
|
449
|
+
end: later.last,
|
|
450
|
+
fromId: earlier.firstId,
|
|
451
|
+
toId: later.lastId,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
intervals.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
456
|
+
const merged: SuppressionInterval[] = [];
|
|
457
|
+
for (const interval of intervals) {
|
|
458
|
+
const previous = merged.at(-1);
|
|
459
|
+
if (previous && interval.start <= previous.end + 1) {
|
|
460
|
+
if (interval.end > previous.end) {
|
|
461
|
+
previous.end = interval.end;
|
|
462
|
+
previous.toId = interval.toId;
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
merged.push({ ...interval });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const refs: DiscordAwarenessRef[] = [];
|
|
470
|
+
for (const interval of merged) {
|
|
471
|
+
refs.push(...collectDiscordRefs(
|
|
472
|
+
contextManager,
|
|
473
|
+
interval.start,
|
|
474
|
+
interval.end + 1,
|
|
475
|
+
forcedServerId,
|
|
476
|
+
));
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
intervals: merged,
|
|
480
|
+
refs: dedupeDiscordRefs(refs),
|
|
481
|
+
messageCount: merged.reduce((sum, interval) => sum + interval.end - interval.start + 1, 0),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function collectDiscordRefs(
|
|
486
|
+
contextManager: ContextManager,
|
|
487
|
+
start: number,
|
|
488
|
+
end: number,
|
|
489
|
+
forcedServerId?: string,
|
|
490
|
+
): DiscordAwarenessRef[] {
|
|
491
|
+
const refs: DiscordAwarenessRef[] = [];
|
|
492
|
+
for (let offset = start; offset < end; offset += RECOVERY_SCAN_WINDOW) {
|
|
493
|
+
const messages = contextManager.getMessageWindow(
|
|
494
|
+
offset,
|
|
495
|
+
Math.min(RECOVERY_SCAN_WINDOW, end - offset),
|
|
496
|
+
{ resolveBlobs: false },
|
|
497
|
+
).messages;
|
|
498
|
+
refs.push(...extractDiscordAwarenessRefs(messages, forcedServerId));
|
|
499
|
+
}
|
|
500
|
+
// A sharded message may repeat Discord metadata across adjacent records.
|
|
501
|
+
return dedupeDiscordRefs(refs);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function dedupeDiscordRefs(refs: DiscordAwarenessRef[]): DiscordAwarenessRef[] {
|
|
505
|
+
const deduped = new Map<string, DiscordAwarenessRef>();
|
|
506
|
+
for (const ref of refs) {
|
|
507
|
+
deduped.set(`${ref.serverId}\0${ref.channelId}\0${ref.messageId}`, ref);
|
|
508
|
+
}
|
|
509
|
+
return [...deduped.values()];
|
|
510
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createOfflineRecoveryBranch } from './offline-branch.js';
|
|
4
|
+
|
|
5
|
+
interface CliOptions {
|
|
6
|
+
storePath?: string;
|
|
7
|
+
agentName?: string;
|
|
8
|
+
messageId?: string;
|
|
9
|
+
contextId?: string;
|
|
10
|
+
messages?: number;
|
|
11
|
+
suppressMessageIds: string[];
|
|
12
|
+
suppressRanges: Array<{ fromMessageId: string; toMessageId: string }>;
|
|
13
|
+
branchName?: string;
|
|
14
|
+
namespace?: string;
|
|
15
|
+
discordServerId?: string;
|
|
16
|
+
outboxPath?: string;
|
|
17
|
+
emoji?: string;
|
|
18
|
+
dryRun: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function usage(): string {
|
|
22
|
+
return `Usage:
|
|
23
|
+
agent-framework-recover --store <path> --agent <name> <anchor> [options]
|
|
24
|
+
|
|
25
|
+
Creates and activates a Chronicle recovery branch while the normal host is down,
|
|
26
|
+
then queues Discord reactions for addressable messages removed from awareness.
|
|
27
|
+
Message content is never compiled or written to the recovery outbox.
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--message-id <id|link> Last Discord message the agent should still see
|
|
31
|
+
--context-id <internal-id> Exact internal context message to retain as anchor
|
|
32
|
+
--messages <count> Legacy alternative: remove N context entries
|
|
33
|
+
--suppress <id|link> Suppress one Discord message; repeat or comma-separate
|
|
34
|
+
--suppress-range <a>..<b> Suppress an inclusive context range; repeatable
|
|
35
|
+
--branch <name> Recovery branch name (generated by default)
|
|
36
|
+
--namespace <namespace> Context namespace (default: agents/<agent>)
|
|
37
|
+
--discord-server <id> Override/inject the Discord MCPL server id
|
|
38
|
+
--outbox <path> Override the branch-independent outbox path
|
|
39
|
+
--emoji <emoji> Marker reaction (default: 💤)
|
|
40
|
+
--dry-run Inspect counts/addresses without writing
|
|
41
|
+
--help Show this help
|
|
42
|
+
|
|
43
|
+
The agent host must be stopped before running this command.`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseArgs(args: string[]): CliOptions {
|
|
47
|
+
const options: CliOptions = { dryRun: false, suppressMessageIds: [], suppressRanges: [] };
|
|
48
|
+
for (let i = 0; i < args.length; i++) {
|
|
49
|
+
const arg = args[i];
|
|
50
|
+
const value = () => {
|
|
51
|
+
const next = args[++i];
|
|
52
|
+
if (!next) throw new Error(`${arg} requires a value`);
|
|
53
|
+
return next;
|
|
54
|
+
};
|
|
55
|
+
switch (arg) {
|
|
56
|
+
case '--store': options.storePath = value(); break;
|
|
57
|
+
case '--agent': options.agentName = value(); break;
|
|
58
|
+
case '--message-id':
|
|
59
|
+
case '--message': options.messageId = value(); break;
|
|
60
|
+
case '--context-id': options.contextId = value(); break;
|
|
61
|
+
case '--messages': options.messages = Number(value()); break;
|
|
62
|
+
case '--suppress':
|
|
63
|
+
case '--suppress-message':
|
|
64
|
+
options.suppressMessageIds.push(...value().split(',').map((item) => item.trim()).filter(Boolean));
|
|
65
|
+
break;
|
|
66
|
+
case '--suppress-range': {
|
|
67
|
+
const raw = value();
|
|
68
|
+
const separator = raw.indexOf('..');
|
|
69
|
+
if (separator <= 0 || separator >= raw.length - 2) {
|
|
70
|
+
throw new Error('--suppress-range must be formatted as <first>..<last>');
|
|
71
|
+
}
|
|
72
|
+
options.suppressRanges.push({
|
|
73
|
+
fromMessageId: raw.slice(0, separator),
|
|
74
|
+
toMessageId: raw.slice(separator + 2),
|
|
75
|
+
});
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case '--branch': options.branchName = value(); break;
|
|
79
|
+
case '--namespace': options.namespace = value(); break;
|
|
80
|
+
case '--discord-server': options.discordServerId = value(); break;
|
|
81
|
+
case '--outbox': options.outboxPath = value(); break;
|
|
82
|
+
case '--emoji': options.emoji = value(); break;
|
|
83
|
+
case '--dry-run': options.dryRun = true; break;
|
|
84
|
+
case '--help':
|
|
85
|
+
case '-h':
|
|
86
|
+
console.log(usage());
|
|
87
|
+
process.exit(0);
|
|
88
|
+
default: throw new Error(`Unknown argument: ${arg}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return options;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function main(): Promise<void> {
|
|
95
|
+
const options = parseArgs(process.argv.slice(2));
|
|
96
|
+
if (
|
|
97
|
+
!options.storePath
|
|
98
|
+
|| !options.agentName
|
|
99
|
+
|| (!options.messageId && !options.contextId && options.messages === undefined)
|
|
100
|
+
) {
|
|
101
|
+
throw new Error(`--store, --agent, and one anchor (--message-id, --context-id, or --messages) are required\n\n${usage()}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = await createOfflineRecoveryBranch({
|
|
105
|
+
storePath: options.storePath,
|
|
106
|
+
agentName: options.agentName,
|
|
107
|
+
messageId: options.messageId,
|
|
108
|
+
contextId: options.contextId,
|
|
109
|
+
messages: options.messages,
|
|
110
|
+
suppressMessageIds: options.suppressMessageIds,
|
|
111
|
+
suppressRanges: options.suppressRanges,
|
|
112
|
+
branchName: options.branchName,
|
|
113
|
+
namespace: options.namespace,
|
|
114
|
+
discordServerId: options.discordServerId,
|
|
115
|
+
outboxPath: options.outboxPath,
|
|
116
|
+
emoji: options.emoji,
|
|
117
|
+
dryRun: options.dryRun,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
console.log(JSON.stringify({
|
|
121
|
+
...result,
|
|
122
|
+
// Keep the output useful for ops without ever exposing message content.
|
|
123
|
+
refs: result.refs.map(({ serverId, channelId, messageId }) => ({ serverId, channelId, messageId })),
|
|
124
|
+
}, null, 2));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
main().catch((error) => {
|
|
128
|
+
console.error(`[agent-framework-recover] ${error instanceof Error ? error.message : String(error)}`);
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
});
|
package/src/types/agent.ts
CHANGED
|
@@ -2,6 +2,12 @@ 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
4
|
|
|
5
|
+
export type SameRoundThinkTextPolicy = 'public' | 'private';
|
|
6
|
+
export type SameRoundThinkTextPolicySource =
|
|
7
|
+
| 'runtime_override'
|
|
8
|
+
| 'recipe'
|
|
9
|
+
| 'compatibility_default';
|
|
10
|
+
|
|
5
11
|
/**
|
|
6
12
|
* Configuration for an agent.
|
|
7
13
|
*/
|
|
@@ -65,6 +71,11 @@ export interface AgentConfig {
|
|
|
65
71
|
/** Provider-specific request parameters (for example Responses reasoning
|
|
66
72
|
* and server-side compaction settings). */
|
|
67
73
|
providerParams?: Record<string, unknown>;
|
|
74
|
+
/**
|
|
75
|
+
* Same-round routing policy for ordinary text emitted beside `think`.
|
|
76
|
+
* Omitted preserves the compatibility carry-forward: public.
|
|
77
|
+
*/
|
|
78
|
+
sameRoundThinkTextPolicy?: SameRoundThinkTextPolicy;
|
|
68
79
|
/**
|
|
69
80
|
* Extended thinking config. When `enabled: true`, the agent runs with
|
|
70
81
|
* Anthropic's native extended thinking; responses include `thinking` blocks
|
|
@@ -104,9 +115,42 @@ export interface AgentConfig {
|
|
|
104
115
|
* than dropping it silently. Default true.
|
|
105
116
|
*/
|
|
106
117
|
announceHumanTurns?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Primary-lane autobiographical summary refusal fallback. Disabled by
|
|
120
|
+
* default: when enabled, the framework may compare the refused primary
|
|
121
|
+
* request against the latest compatible healthy request on the same branch
|
|
122
|
+
* generation, expand newly admitted summary pairs back to exact raw
|
|
123
|
+
* source, and issue at most one retry.
|
|
124
|
+
*/
|
|
125
|
+
primarySummaryFallback?: {
|
|
126
|
+
enabled?: boolean;
|
|
127
|
+
maxNewSummaries?: number;
|
|
128
|
+
requestBudgetTokens?: number;
|
|
129
|
+
};
|
|
107
130
|
};
|
|
108
131
|
}
|
|
109
132
|
|
|
133
|
+
/** The intentionally small set of agent settings that may change in-process. */
|
|
134
|
+
export interface AgentRuntimeSettingsPatch {
|
|
135
|
+
contextBudgetTokens?: number;
|
|
136
|
+
tailTokens?: number;
|
|
137
|
+
transitionPaceTokens?: number;
|
|
138
|
+
sameRoundThinkTextPolicy?: SameRoundThinkTextPolicy;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface AgentRuntimeSettingsSnapshot {
|
|
142
|
+
contextBudgetTokens: number;
|
|
143
|
+
tailTokens?: number;
|
|
144
|
+
transitionPaceTokens?: number;
|
|
145
|
+
sameRoundThinkTextPolicy: SameRoundThinkTextPolicy;
|
|
146
|
+
sameRoundThinkTextPolicySource: SameRoundThinkTextPolicySource;
|
|
147
|
+
transition: 'stable' | 'converging' | 'blocked';
|
|
148
|
+
transitionReason?: 'transition_pace_too_small' | 'protected_context_exceeds_target';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Persisted values differ from recipe defaults; omitted keys inherit config. */
|
|
152
|
+
export type AgentRuntimeSettingsOverrides = AgentRuntimeSettingsPatch;
|
|
153
|
+
|
|
110
154
|
/**
|
|
111
155
|
* Result of running inference.
|
|
112
156
|
*/
|