odai-dsh-agent 0.2.1 → 0.2.3

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.
Files changed (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +138 -138
  3. package/bin/odai-dsh-agent.mjs +108 -108
  4. package/package.json +69 -69
  5. package/preset/odai/agent.cordis.yml +261 -260
  6. package/preset/odai/preset.yml +3 -3
  7. package/preset/odai/runtime/compaction-config.mjs +388 -388
  8. package/preset/odai/runtime/governance.mjs +128 -128
  9. package/preset/odai/runtime/index.mjs +2461 -2263
  10. package/preset/odai/runtime/output-config.mjs +360 -304
  11. package/preset/odai/runtime/research-packet.mjs +150 -150
  12. package/preset/odai/runtime/responsibility-scope.mjs +35 -2
  13. package/preset/odai/runtime/role-overlays.mjs +27 -27
  14. package/preset/odai/runtime/route-card.mjs +168 -152
  15. package/preset/odai/runtime/router.mjs +683 -651
  16. package/preset/odai/runtime/routing-config.mjs +480 -436
  17. package/preset/odai/runtime/routing-context.mjs +321 -321
  18. package/preset/odai/runtime/semantic-memory-store.mjs +452 -452
  19. package/preset/odai/runtime/semantic-memory.mjs +842 -842
  20. package/preset/odai/runtime/session-compat.mjs +389 -389
  21. package/preset/odai/runtime/skill-bundle.mjs +318 -318
  22. package/preset/odai/runtime/skill-evolution.mjs +1413 -1413
  23. package/preset/odai/skills/odai/agents/openai.yaml +4 -4
  24. package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -11
  25. package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -11
  26. package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -5
  27. package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -13
  28. package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -24
  29. package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -15
  30. package/preset/odai/skills/odai/assets/routing-roles/executor.md +11 -11
  31. package/preset/odai/skills/odai/assets/routing-roles/frontend.md +7 -7
  32. package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -29
  33. package/preset/odai/skills/odai/assets/routing-roles/researcher.md +6 -6
  34. package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
  35. package/preset/odai/skills/odai/assets/task-state.md +14 -14
  36. package/preset/odai/skills/odai/manifest.json +37 -37
  37. package/preset/odai/skills/odai/references/craft.md +54 -54
  38. package/preset/odai/skills/odai/references/dao.md +30 -30
  39. package/preset/odai/skills/odai/references/leverage.md +53 -53
  40. package/preset/odai/skills/odai/references/planning.md +82 -82
  41. package/preset/odai/skills/odai/references/support.md +36 -36
  42. package/preset/odai/skills/odai/references/verification.md +23 -23
  43. package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -273
  44. package/preset/odai/skills/odai/scripts/build-routing.mjs +274 -274
  45. package/preset/odai/skills/odai/scripts/install-routing.mjs +397 -397
  46. package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -457
  47. package/preset/odai/skills/odai/scripts/run-role.mjs +336 -336
  48. package/preset/odai/skills/odai/scripts/run-routing.mjs +1154 -1154
  49. package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -103
  50. package/scripts/verify-agent-load.mjs +257 -257
  51. package/src/installer.mjs +398 -382
@@ -1,321 +1,321 @@
1
- import { createHash } from "node:crypto";
2
-
3
- const DEFAULT_MAX_CHARS = 12_000;
4
- const DEFAULT_MAX_EVENTS = 80;
5
- const REQUIREMENT_PATTERNS = [/requirement/iu, /acceptance/iu, /需求/u, /要求/u, /目标/u, /验收/u];
6
- const ACCEPTANCE_PATTERNS = [/\bA\d+\b/u, /accept(?:ance)?/iu, /验收/u, /通过条件/u, /完成条件/u];
7
- const DIFF_OUTPUT_PATTERNS = [/diff --git/iu, /^---\s+a\/[^\n]+\n\+\+\+\s+b\//imu];
8
- const DIFF_COMMAND_PATTERNS = [/^\s*git(?:\.exe)?\s+diff(?:\s|$)/iu];
9
- const TEST_COMMAND_PATTERNS = [
10
- /^\s*(?:&\s*)?(?:"[^"\r\n]*[\\/]node(?:\.exe)?"|[A-Za-z]:\\[^\s"\r\n]*[\\/]node(?:\.exe)?|\/[^\s"\r\n]*\/node|node(?:\.exe)?)\s+--test(?:\s|$)/iu,
11
- /^\s*(?:npm|pnpm|yarn)(?:\.cmd)?(?:\s+--(?:prefix|dir)\s+(?:"[^"]+"|\S+))?\s+(?:run\s+)?test(?:\s|$)/iu,
12
- /^\s*(?:pytest|vitest|jest|mocha)(?:\.cmd)?(?:\s|$)/iu,
13
- /^\s*(?:go|cargo|dotnet)\s+test(?:\s|$)/iu,
14
- ];
15
- const TEST_SUCCESS_PATTERNS = [
16
- /exit code:\s*0/iu,
17
- /(?:^|\n)[^\n]*(?:pass|passed)\s*[:=]?\s*[1-9]\d*/iu,
18
- /(?:^|\n)[^\n]*[1-9]\d*\s+passed\b/iu,
19
- /(?:^|\n)[^\n]*fail(?:ed)?\s*[:=]?\s*0\b/iu,
20
- /\btests?\s+\d+[^\n]*(?:pass|passed)\s+[1-9]\d*[^\n]*fail(?:ed)?\s+0\b/iu,
21
- ];
22
- const TEST_FAILURE_PATTERNS = [
23
- /exit code:\s*[1-9]\d*/iu,
24
- /(?:^|\n)[^\n]*fail(?:ed)?\s*[:=]?\s*[1-9]\d*/iu,
25
- /\b(?:tests?|test suite) failed\b/iu,
26
- ];
27
- const WRITE_TOOL_NAMES = new Set(["edit", "write", "apply_patch"]);
28
- const SHELL_TOOL_NAMES = new Set(["bash", "pwsh", "shell", "powershell"]);
29
- const SHELL_CONTROL_PATTERN = /[;&|<>]/u;
30
- const READ_ONLY_SHELL_COMMAND_PATTERNS = [
31
- /^\s*git(?:\.exe)?\s+(?:status|log|show|rev-parse|ls-files|branch)(?:\s|$)/iu,
32
- /^\s*(?:Get-[A-Za-z]+|Select-String|Test-Path)(?:\s|$)/iu,
33
- /^\s*(?:rg|grep|find|ls|pwd|cat|head|tail|wc|where|which)(?:\.exe)?(?:\s|$)/iu,
34
- /^\s*node(?:\.exe)?\s+--check(?:\s|$)/iu,
35
- ];
36
- const WRITE_COMMAND_PATTERNS = [
37
- /(?:^|\s)(?:git\s+apply|apply_patch|sed\s+-i|perl\s+-pi)(?:\s|$)/iu,
38
- /(?:^|\s)(?:Set-Content|Add-Content|Out-File|Remove-Item|Move-Item|Copy-Item|New-Item)(?:\s|$)/iu,
39
- /(?:^|\s)(?:rm|mv|cp|mkdir|touch)(?:\s|$)/u,
40
- ];
41
-
42
- function textBlocks(value, depth = 0) {
43
- if (depth > 5 || value === null || value === undefined) return [];
44
- if (typeof value === "string") return [value];
45
- if (Array.isArray(value)) return value.flatMap((item) => textBlocks(item, depth + 1));
46
- if (typeof value !== "object") return [];
47
- if (value.type === "text" && typeof value.text === "string") return [value.text];
48
- const keys = ["content", "message", "output", "result", "data", "text", "value", "command", "args", "input"];
49
- return keys.flatMap((key) => key in value ? textBlocks(value[key], depth + 1) : []);
50
- }
51
-
52
- function matchesAny(text, patterns) {
53
- return patterns.some((pattern) => pattern.test(text));
54
- }
55
-
56
- function toolResultBlocks(value, depth = 0) {
57
- if (depth > 5 || value === null || value === undefined) return [];
58
- if (Array.isArray(value)) return value.flatMap((item) => toolResultBlocks(item, depth + 1));
59
- if (typeof value !== "object") return [];
60
- const own = value.type === "tool-result" ? [value] : [];
61
- return own.concat(Object.values(value).flatMap((item) => toolResultBlocks(item, depth + 1)));
62
- }
63
-
64
- function nativeToolResult(data) {
65
- const blocks = toolResultBlocks(data);
66
- if (blocks.length !== 1) return undefined;
67
- const block = blocks[0];
68
- const source = data?.message?.source;
69
- if (source?.kind !== "tool"
70
- || typeof source.callId !== "string"
71
- || !source.callId
72
- || block.toolCallId !== source.callId
73
- || typeof block.isError !== "boolean"
74
- || !Array.isArray(block.content)) {
75
- return undefined;
76
- }
77
- return Object.freeze({
78
- callId: source.callId,
79
- successful: block.isError === false && !data?.error,
80
- });
81
- }
82
-
83
- function commandFromCall(call) {
84
- const args = call?.arguments;
85
- if (typeof args === "string") return args;
86
- if (!args || typeof args !== "object" || Array.isArray(args)) return "";
87
- for (const field of ["command", "cmd", "script"]) {
88
- if (typeof args[field] === "string") return args[field];
89
- }
90
- return "";
91
- }
92
-
93
- function resultReferencesCall(event, call) {
94
- return Number.isSafeInteger(call?.seq)
95
- && Array.isArray(event?.sourceEventSeqs)
96
- && event.sourceEventSeqs.length === 1
97
- && event.sourceEventSeqs[0] === call.seq;
98
- }
99
-
100
- function nativeToolCalls(events, startIndex) {
101
- const calls = new Map();
102
- const duplicates = new Set();
103
- for (let index = startIndex; index < events.length; index += 1) {
104
- const event = events[index];
105
- if (event?.type !== "tool/call") continue;
106
- const callId = event.data?.callId;
107
- const name = event.data?.name;
108
- if (typeof callId !== "string" || !callId || typeof name !== "string" || !name || !Number.isSafeInteger(event.seq)) continue;
109
- if (calls.has(callId)) {
110
- calls.delete(callId);
111
- duplicates.add(callId);
112
- continue;
113
- }
114
- if (duplicates.has(callId)) continue;
115
- calls.set(callId, Object.freeze({
116
- index,
117
- seq: event.seq,
118
- name,
119
- arguments: event.data?.arguments,
120
- }));
121
- }
122
- return calls;
123
- }
124
-
125
- function eventEvidence(event, index, calls) {
126
- if (event?.type === "user/message") {
127
- const text = textBlocks(event.data).join("\n").trim();
128
- return text ? { index, source: "user", kinds: ["requirement"], label: "earlier user requirement", text } : undefined;
129
- }
130
- if (["assistant/message", "agent/message", "message/assistant"].includes(event?.type)) {
131
- const text = textBlocks(event.data).join("\n").trim();
132
- if (!text) return undefined;
133
- const kinds = ["assistant-claim"];
134
- if (matchesAny(text, REQUIREMENT_PATTERNS)) kinds.push("requirement");
135
- if (matchesAny(text, ACCEPTANCE_PATTERNS)) kinds.push("acceptance");
136
- return { index, source: "assistant", kinds, label: "controller claim", text };
137
- }
138
- if (event?.type === "odai/route-card-frozen" && event.data?.card) {
139
- return {
140
- index,
141
- source: "route-card",
142
- kinds: ["acceptance"],
143
- label: `frozen route card ${event.data.card.id ?? "(unknown)"}`,
144
- text: JSON.stringify(event.data.card),
145
- };
146
- }
147
- if (event?.type === "tool/result") {
148
- const result = nativeToolResult(event.data);
149
- const call = result ? calls.get(result.callId) : undefined;
150
- if (!call || call.index >= index || !resultReferencesCall(event, call)) return undefined;
151
- const callId = result.callId;
152
- const successful = result.successful;
153
- const command = commandFromCall(call);
154
- const simpleCommand = !SHELL_CONTROL_PATTERN.test(command);
155
- const diffCommand = simpleCommand && matchesAny(command, DIFF_COMMAND_PATTERNS);
156
- const testCommand = simpleCommand && matchesAny(command, TEST_COMMAND_PATTERNS);
157
- const explicitWrite = WRITE_TOOL_NAMES.has(call.name) || matchesAny(command, WRITE_COMMAND_PATTERNS);
158
- const unknownShellMutation = SHELL_TOOL_NAMES.has(call.name)
159
- && !diffCommand
160
- && !testCommand
161
- && (SHELL_CONTROL_PATTERN.test(command) || !matchesAny(command, READ_ONLY_SHELL_COMMAND_PATTERNS));
162
- const writeCommand = explicitWrite || unknownShellMutation;
163
- if (!successful && !testCommand && !writeCommand) return undefined;
164
- const output = textBlocks(event.data).join("\n").trim();
165
- const kinds = successful ? ["tool"] : [];
166
- if (successful && diffCommand && matchesAny(output, DIFF_OUTPUT_PATTERNS)) kinds.push("diff");
167
- if (writeCommand) kinds.push("write");
168
- if (testCommand) {
169
- const testPassed = successful
170
- && matchesAny(output, TEST_SUCCESS_PATTERNS)
171
- && !matchesAny(output, TEST_FAILURE_PATTERNS);
172
- kinds.push(testPassed ? "test" : "test-failed");
173
- }
174
- const identity = `tool-call:${callId}`;
175
- const text = [command ? `command: ${command}` : "", output || "(no textual result)"].filter(Boolean).join("\n\n");
176
- return {
177
- index,
178
- source: "tool",
179
- kinds,
180
- label: `tool ${call.name} (${identity})`,
181
- identity,
182
- text,
183
- };
184
- }
185
- if (event?.type === "odai/tool-observed" && event.data?.isError === false) {
186
- const identity = typeof event.data.callId === "string" ? `tool-call:${event.data.callId}` : undefined;
187
- const tool = event.data.tool ?? "unknown";
188
- return {
189
- index,
190
- source: "tool",
191
- kinds: ["tool", ...(WRITE_TOOL_NAMES.has(tool) ? ["write"] : [])],
192
- label: `observed tool ${tool}${identity ? ` (${identity})` : ""}`,
193
- ...(identity ? { identity } : {}),
194
- text: JSON.stringify(event.data),
195
- };
196
- }
197
- return undefined;
198
- }
199
-
200
- function truncateText(text, limit) {
201
- if (text.length <= limit) return { text, truncated: false };
202
- return { text: `${text.slice(0, Math.max(0, limit - 24))}\n...[packet truncated]`, truncated: true };
203
- }
204
-
205
- function digestPacket(value) {
206
- return createHash("sha256").update(JSON.stringify(value)).digest("hex");
207
- }
208
-
209
- function coverageFor(currentTask, entries) {
210
- const matching = (kind) => entries.filter((entry) => entry.kinds.includes(kind));
211
- const latestIndex = (kind) => matching(kind).reduce((latest, entry) => Math.max(latest, entry.index), -1);
212
- const acceptanceCount = matching("acceptance").length;
213
- const diffEntries = matching("diff");
214
- const testEntries = matching("test");
215
- const failedTestEntries = matching("test-failed");
216
- const latestWriteIndex = latestIndex("write");
217
- const latestDiffIndex = latestIndex("diff");
218
- const latestTestIndex = latestIndex("test");
219
- const latestFailedTestIndex = latestIndex("test-failed");
220
- const currentEvidence = acceptanceCount > 0
221
- && latestDiffIndex >= 0
222
- && latestTestIndex >= 0
223
- && latestDiffIndex > latestWriteIndex
224
- && latestTestIndex > latestWriteIndex
225
- && latestTestIndex > latestFailedTestIndex
226
- && diffEntries.some((diff) => testEntries.some((testResult) => diff.identity !== testResult.identity));
227
- return Object.freeze({
228
- requirements: Boolean(currentTask) || matching("requirement").length > 0,
229
- acceptanceCount,
230
- diffCount: diffEntries.length,
231
- testCount: testEntries.length,
232
- failedTestCount: failedTestEntries.length,
233
- writeCount: matching("write").length,
234
- toolEvidenceCount: matching("tool").length,
235
- latestWriteIndex,
236
- latestDiffIndex,
237
- latestTestIndex,
238
- latestFailedTestIndex,
239
- currentEvidence,
240
- });
241
- }
242
-
243
- export function buildRoleContextPacket(agent, role, taskText, options = {}) {
244
- const maxChars = Number.isSafeInteger(options.maxChars) && options.maxChars > 0
245
- ? options.maxChars
246
- : DEFAULT_MAX_CHARS;
247
- const maxEvents = Number.isSafeInteger(options.maxEvents) && options.maxEvents > 0
248
- ? options.maxEvents
249
- : DEFAULT_MAX_EVENTS;
250
- const taskBudget = Math.max(128, Math.floor(maxChars / 2));
251
- const task = truncateText(String(taskText ?? "").trim(), taskBudget);
252
- const events = Array.isArray(agent?.session?.events) ? agent.session.events : [];
253
- const startIndex = Math.max(0, events.length - maxEvents);
254
- const calls = nativeToolCalls(events, startIndex);
255
- const selected = [];
256
- let evidenceChars = 0;
257
- let truncated = task.truncated || events.length > maxEvents;
258
-
259
- for (let index = events.length - 1; index >= startIndex; index -= 1) {
260
- const entry = eventEvidence(events[index], index, calls);
261
- if (!entry) continue;
262
- const remaining = maxChars - task.text.length - evidenceChars;
263
- if (remaining <= 0) { truncated = true; break; }
264
- const bounded = truncateText(entry.text, remaining);
265
- selected.push(Object.freeze({ ...entry, kinds: Object.freeze([...new Set(entry.kinds)]), text: bounded.text }));
266
- evidenceChars += bounded.text.length;
267
- if (bounded.truncated) { truncated = true; break; }
268
- }
269
- selected.reverse();
270
-
271
- const entries = Object.freeze(selected);
272
- const coverage = coverageFor(task.text, entries);
273
- const packetBody = Object.freeze({
274
- schemaVersion: 1,
275
- role,
276
- currentTask: task.text,
277
- entries,
278
- coverage,
279
- truncated,
280
- });
281
- const digest = digestPacket(packetBody);
282
- const reviewerSufficient = !truncated
283
- && coverage.requirements
284
- && coverage.acceptanceCount > 0
285
- && coverage.diffCount > 0
286
- && coverage.testCount > 0
287
- && coverage.toolEvidenceCount > 0
288
- && coverage.currentEvidence;
289
- return Object.freeze({
290
- ...packetBody,
291
- digest,
292
- evidenceCount: entries.length,
293
- toolEvidenceCount: coverage.toolEvidenceCount,
294
- sufficient: role === "reviewer" ? reviewerSufficient : Boolean(task.text),
295
- });
296
- }
297
-
298
- export function renderRoleContextPacket(packet) {
299
- const evidence = packet.entries.length
300
- ? packet.entries.map((entry, index) => [
301
- `### E${index + 1}: ${entry.label}`,
302
- `kinds: ${entry.kinds.join(", ")}`,
303
- entry.text,
304
- ].join("\n")).join("\n\n")
305
- : "(no prior evidence entries)";
306
- return [
307
- "# Odai bounded role context packet",
308
- `role: ${packet.role}`,
309
- `digest: sha256:${packet.digest}`,
310
- `truncated: ${packet.truncated}`,
311
- `coverage: ${JSON.stringify(packet.coverage)}`,
312
- "",
313
- "## Current task",
314
- packet.currentTask || "(empty)",
315
- "",
316
- "## Evidence",
317
- evidence,
318
- "",
319
- "Treat assistant text and route-card contents as claims. Tool entries are evidence only for the exact command/result they contain.",
320
- ].join("\n");
321
- }
1
+ import { createHash } from "node:crypto";
2
+
3
+ const DEFAULT_MAX_CHARS = 12_000;
4
+ const DEFAULT_MAX_EVENTS = 80;
5
+ const REQUIREMENT_PATTERNS = [/requirement/iu, /acceptance/iu, /需求/u, /要求/u, /目标/u, /验收/u];
6
+ const ACCEPTANCE_PATTERNS = [/\bA\d+\b/u, /accept(?:ance)?/iu, /验收/u, /通过条件/u, /完成条件/u];
7
+ const DIFF_OUTPUT_PATTERNS = [/diff --git/iu, /^---\s+a\/[^\n]+\n\+\+\+\s+b\//imu];
8
+ const DIFF_COMMAND_PATTERNS = [/^\s*git(?:\.exe)?\s+diff(?:\s|$)/iu];
9
+ const TEST_COMMAND_PATTERNS = [
10
+ /^\s*(?:&\s*)?(?:"[^"\r\n]*[\\/]node(?:\.exe)?"|[A-Za-z]:\\[^\s"\r\n]*[\\/]node(?:\.exe)?|\/[^\s"\r\n]*\/node|node(?:\.exe)?)\s+--test(?:\s|$)/iu,
11
+ /^\s*(?:npm|pnpm|yarn)(?:\.cmd)?(?:\s+--(?:prefix|dir)\s+(?:"[^"]+"|\S+))?\s+(?:run\s+)?test(?:\s|$)/iu,
12
+ /^\s*(?:pytest|vitest|jest|mocha)(?:\.cmd)?(?:\s|$)/iu,
13
+ /^\s*(?:go|cargo|dotnet)\s+test(?:\s|$)/iu,
14
+ ];
15
+ const TEST_SUCCESS_PATTERNS = [
16
+ /exit code:\s*0/iu,
17
+ /(?:^|\n)[^\n]*(?:pass|passed)\s*[:=]?\s*[1-9]\d*/iu,
18
+ /(?:^|\n)[^\n]*[1-9]\d*\s+passed\b/iu,
19
+ /(?:^|\n)[^\n]*fail(?:ed)?\s*[:=]?\s*0\b/iu,
20
+ /\btests?\s+\d+[^\n]*(?:pass|passed)\s+[1-9]\d*[^\n]*fail(?:ed)?\s+0\b/iu,
21
+ ];
22
+ const TEST_FAILURE_PATTERNS = [
23
+ /exit code:\s*[1-9]\d*/iu,
24
+ /(?:^|\n)[^\n]*fail(?:ed)?\s*[:=]?\s*[1-9]\d*/iu,
25
+ /\b(?:tests?|test suite) failed\b/iu,
26
+ ];
27
+ const WRITE_TOOL_NAMES = new Set(["edit", "write", "apply_patch"]);
28
+ const SHELL_TOOL_NAMES = new Set(["bash", "pwsh", "shell", "powershell"]);
29
+ const SHELL_CONTROL_PATTERN = /[;&|<>]/u;
30
+ const READ_ONLY_SHELL_COMMAND_PATTERNS = [
31
+ /^\s*git(?:\.exe)?\s+(?:status|log|show|rev-parse|ls-files|branch)(?:\s|$)/iu,
32
+ /^\s*(?:Get-[A-Za-z]+|Select-String|Test-Path)(?:\s|$)/iu,
33
+ /^\s*(?:rg|grep|find|ls|pwd|cat|head|tail|wc|where|which)(?:\.exe)?(?:\s|$)/iu,
34
+ /^\s*node(?:\.exe)?\s+--check(?:\s|$)/iu,
35
+ ];
36
+ const WRITE_COMMAND_PATTERNS = [
37
+ /(?:^|\s)(?:git\s+apply|apply_patch|sed\s+-i|perl\s+-pi)(?:\s|$)/iu,
38
+ /(?:^|\s)(?:Set-Content|Add-Content|Out-File|Remove-Item|Move-Item|Copy-Item|New-Item)(?:\s|$)/iu,
39
+ /(?:^|\s)(?:rm|mv|cp|mkdir|touch)(?:\s|$)/u,
40
+ ];
41
+
42
+ function textBlocks(value, depth = 0) {
43
+ if (depth > 5 || value === null || value === undefined) return [];
44
+ if (typeof value === "string") return [value];
45
+ if (Array.isArray(value)) return value.flatMap((item) => textBlocks(item, depth + 1));
46
+ if (typeof value !== "object") return [];
47
+ if (value.type === "text" && typeof value.text === "string") return [value.text];
48
+ const keys = ["content", "message", "output", "result", "data", "text", "value", "command", "args", "input"];
49
+ return keys.flatMap((key) => key in value ? textBlocks(value[key], depth + 1) : []);
50
+ }
51
+
52
+ function matchesAny(text, patterns) {
53
+ return patterns.some((pattern) => pattern.test(text));
54
+ }
55
+
56
+ function toolResultBlocks(value, depth = 0) {
57
+ if (depth > 5 || value === null || value === undefined) return [];
58
+ if (Array.isArray(value)) return value.flatMap((item) => toolResultBlocks(item, depth + 1));
59
+ if (typeof value !== "object") return [];
60
+ const own = value.type === "tool-result" ? [value] : [];
61
+ return own.concat(Object.values(value).flatMap((item) => toolResultBlocks(item, depth + 1)));
62
+ }
63
+
64
+ function nativeToolResult(data) {
65
+ const blocks = toolResultBlocks(data);
66
+ if (blocks.length !== 1) return undefined;
67
+ const block = blocks[0];
68
+ const source = data?.message?.source;
69
+ if (source?.kind !== "tool"
70
+ || typeof source.callId !== "string"
71
+ || !source.callId
72
+ || block.toolCallId !== source.callId
73
+ || typeof block.isError !== "boolean"
74
+ || !Array.isArray(block.content)) {
75
+ return undefined;
76
+ }
77
+ return Object.freeze({
78
+ callId: source.callId,
79
+ successful: block.isError === false && !data?.error,
80
+ });
81
+ }
82
+
83
+ function commandFromCall(call) {
84
+ const args = call?.arguments;
85
+ if (typeof args === "string") return args;
86
+ if (!args || typeof args !== "object" || Array.isArray(args)) return "";
87
+ for (const field of ["command", "cmd", "script"]) {
88
+ if (typeof args[field] === "string") return args[field];
89
+ }
90
+ return "";
91
+ }
92
+
93
+ function resultReferencesCall(event, call) {
94
+ return Number.isSafeInteger(call?.seq)
95
+ && Array.isArray(event?.sourceEventSeqs)
96
+ && event.sourceEventSeqs.length === 1
97
+ && event.sourceEventSeqs[0] === call.seq;
98
+ }
99
+
100
+ function nativeToolCalls(events, startIndex) {
101
+ const calls = new Map();
102
+ const duplicates = new Set();
103
+ for (let index = startIndex; index < events.length; index += 1) {
104
+ const event = events[index];
105
+ if (event?.type !== "tool/call") continue;
106
+ const callId = event.data?.callId;
107
+ const name = event.data?.name;
108
+ if (typeof callId !== "string" || !callId || typeof name !== "string" || !name || !Number.isSafeInteger(event.seq)) continue;
109
+ if (calls.has(callId)) {
110
+ calls.delete(callId);
111
+ duplicates.add(callId);
112
+ continue;
113
+ }
114
+ if (duplicates.has(callId)) continue;
115
+ calls.set(callId, Object.freeze({
116
+ index,
117
+ seq: event.seq,
118
+ name,
119
+ arguments: event.data?.arguments,
120
+ }));
121
+ }
122
+ return calls;
123
+ }
124
+
125
+ function eventEvidence(event, index, calls) {
126
+ if (event?.type === "user/message") {
127
+ const text = textBlocks(event.data).join("\n").trim();
128
+ return text ? { index, source: "user", kinds: ["requirement"], label: "earlier user requirement", text } : undefined;
129
+ }
130
+ if (["assistant/message", "agent/message", "message/assistant"].includes(event?.type)) {
131
+ const text = textBlocks(event.data).join("\n").trim();
132
+ if (!text) return undefined;
133
+ const kinds = ["assistant-claim"];
134
+ if (matchesAny(text, REQUIREMENT_PATTERNS)) kinds.push("requirement");
135
+ if (matchesAny(text, ACCEPTANCE_PATTERNS)) kinds.push("acceptance");
136
+ return { index, source: "assistant", kinds, label: "controller claim", text };
137
+ }
138
+ if (event?.type === "odai/route-card-frozen" && event.data?.card) {
139
+ return {
140
+ index,
141
+ source: "route-card",
142
+ kinds: ["acceptance"],
143
+ label: `frozen route card ${event.data.card.id ?? "(unknown)"}`,
144
+ text: JSON.stringify(event.data.card),
145
+ };
146
+ }
147
+ if (event?.type === "tool/result") {
148
+ const result = nativeToolResult(event.data);
149
+ const call = result ? calls.get(result.callId) : undefined;
150
+ if (!call || call.index >= index || !resultReferencesCall(event, call)) return undefined;
151
+ const callId = result.callId;
152
+ const successful = result.successful;
153
+ const command = commandFromCall(call);
154
+ const simpleCommand = !SHELL_CONTROL_PATTERN.test(command);
155
+ const diffCommand = simpleCommand && matchesAny(command, DIFF_COMMAND_PATTERNS);
156
+ const testCommand = simpleCommand && matchesAny(command, TEST_COMMAND_PATTERNS);
157
+ const explicitWrite = WRITE_TOOL_NAMES.has(call.name) || matchesAny(command, WRITE_COMMAND_PATTERNS);
158
+ const unknownShellMutation = SHELL_TOOL_NAMES.has(call.name)
159
+ && !diffCommand
160
+ && !testCommand
161
+ && (SHELL_CONTROL_PATTERN.test(command) || !matchesAny(command, READ_ONLY_SHELL_COMMAND_PATTERNS));
162
+ const writeCommand = explicitWrite || unknownShellMutation;
163
+ if (!successful && !testCommand && !writeCommand) return undefined;
164
+ const output = textBlocks(event.data).join("\n").trim();
165
+ const kinds = successful ? ["tool"] : [];
166
+ if (successful && diffCommand && matchesAny(output, DIFF_OUTPUT_PATTERNS)) kinds.push("diff");
167
+ if (writeCommand) kinds.push("write");
168
+ if (testCommand) {
169
+ const testPassed = successful
170
+ && matchesAny(output, TEST_SUCCESS_PATTERNS)
171
+ && !matchesAny(output, TEST_FAILURE_PATTERNS);
172
+ kinds.push(testPassed ? "test" : "test-failed");
173
+ }
174
+ const identity = `tool-call:${callId}`;
175
+ const text = [command ? `command: ${command}` : "", output || "(no textual result)"].filter(Boolean).join("\n\n");
176
+ return {
177
+ index,
178
+ source: "tool",
179
+ kinds,
180
+ label: `tool ${call.name} (${identity})`,
181
+ identity,
182
+ text,
183
+ };
184
+ }
185
+ if (event?.type === "odai/tool-observed" && event.data?.isError === false) {
186
+ const identity = typeof event.data.callId === "string" ? `tool-call:${event.data.callId}` : undefined;
187
+ const tool = event.data.tool ?? "unknown";
188
+ return {
189
+ index,
190
+ source: "tool",
191
+ kinds: ["tool", ...(WRITE_TOOL_NAMES.has(tool) ? ["write"] : [])],
192
+ label: `observed tool ${tool}${identity ? ` (${identity})` : ""}`,
193
+ ...(identity ? { identity } : {}),
194
+ text: JSON.stringify(event.data),
195
+ };
196
+ }
197
+ return undefined;
198
+ }
199
+
200
+ function truncateText(text, limit) {
201
+ if (text.length <= limit) return { text, truncated: false };
202
+ return { text: `${text.slice(0, Math.max(0, limit - 24))}\n...[packet truncated]`, truncated: true };
203
+ }
204
+
205
+ function digestPacket(value) {
206
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
207
+ }
208
+
209
+ function coverageFor(currentTask, entries) {
210
+ const matching = (kind) => entries.filter((entry) => entry.kinds.includes(kind));
211
+ const latestIndex = (kind) => matching(kind).reduce((latest, entry) => Math.max(latest, entry.index), -1);
212
+ const acceptanceCount = matching("acceptance").length;
213
+ const diffEntries = matching("diff");
214
+ const testEntries = matching("test");
215
+ const failedTestEntries = matching("test-failed");
216
+ const latestWriteIndex = latestIndex("write");
217
+ const latestDiffIndex = latestIndex("diff");
218
+ const latestTestIndex = latestIndex("test");
219
+ const latestFailedTestIndex = latestIndex("test-failed");
220
+ const currentEvidence = acceptanceCount > 0
221
+ && latestDiffIndex >= 0
222
+ && latestTestIndex >= 0
223
+ && latestDiffIndex > latestWriteIndex
224
+ && latestTestIndex > latestWriteIndex
225
+ && latestTestIndex > latestFailedTestIndex
226
+ && diffEntries.some((diff) => testEntries.some((testResult) => diff.identity !== testResult.identity));
227
+ return Object.freeze({
228
+ requirements: Boolean(currentTask) || matching("requirement").length > 0,
229
+ acceptanceCount,
230
+ diffCount: diffEntries.length,
231
+ testCount: testEntries.length,
232
+ failedTestCount: failedTestEntries.length,
233
+ writeCount: matching("write").length,
234
+ toolEvidenceCount: matching("tool").length,
235
+ latestWriteIndex,
236
+ latestDiffIndex,
237
+ latestTestIndex,
238
+ latestFailedTestIndex,
239
+ currentEvidence,
240
+ });
241
+ }
242
+
243
+ export function buildRoleContextPacket(agent, role, taskText, options = {}) {
244
+ const maxChars = Number.isSafeInteger(options.maxChars) && options.maxChars > 0
245
+ ? options.maxChars
246
+ : DEFAULT_MAX_CHARS;
247
+ const maxEvents = Number.isSafeInteger(options.maxEvents) && options.maxEvents > 0
248
+ ? options.maxEvents
249
+ : DEFAULT_MAX_EVENTS;
250
+ const taskBudget = Math.max(128, Math.floor(maxChars / 2));
251
+ const task = truncateText(String(taskText ?? "").trim(), taskBudget);
252
+ const events = Array.isArray(agent?.session?.events) ? agent.session.events : [];
253
+ const startIndex = Math.max(0, events.length - maxEvents);
254
+ const calls = nativeToolCalls(events, startIndex);
255
+ const selected = [];
256
+ let evidenceChars = 0;
257
+ let truncated = task.truncated || events.length > maxEvents;
258
+
259
+ for (let index = events.length - 1; index >= startIndex; index -= 1) {
260
+ const entry = eventEvidence(events[index], index, calls);
261
+ if (!entry) continue;
262
+ const remaining = maxChars - task.text.length - evidenceChars;
263
+ if (remaining <= 0) { truncated = true; break; }
264
+ const bounded = truncateText(entry.text, remaining);
265
+ selected.push(Object.freeze({ ...entry, kinds: Object.freeze([...new Set(entry.kinds)]), text: bounded.text }));
266
+ evidenceChars += bounded.text.length;
267
+ if (bounded.truncated) { truncated = true; break; }
268
+ }
269
+ selected.reverse();
270
+
271
+ const entries = Object.freeze(selected);
272
+ const coverage = coverageFor(task.text, entries);
273
+ const packetBody = Object.freeze({
274
+ schemaVersion: 1,
275
+ role,
276
+ currentTask: task.text,
277
+ entries,
278
+ coverage,
279
+ truncated,
280
+ });
281
+ const digest = digestPacket(packetBody);
282
+ const reviewerSufficient = !truncated
283
+ && coverage.requirements
284
+ && coverage.acceptanceCount > 0
285
+ && coverage.diffCount > 0
286
+ && coverage.testCount > 0
287
+ && coverage.toolEvidenceCount > 0
288
+ && coverage.currentEvidence;
289
+ return Object.freeze({
290
+ ...packetBody,
291
+ digest,
292
+ evidenceCount: entries.length,
293
+ toolEvidenceCount: coverage.toolEvidenceCount,
294
+ sufficient: role === "reviewer" ? reviewerSufficient : Boolean(task.text),
295
+ });
296
+ }
297
+
298
+ export function renderRoleContextPacket(packet) {
299
+ const evidence = packet.entries.length
300
+ ? packet.entries.map((entry, index) => [
301
+ `### E${index + 1}: ${entry.label}`,
302
+ `kinds: ${entry.kinds.join(", ")}`,
303
+ entry.text,
304
+ ].join("\n")).join("\n\n")
305
+ : "(no prior evidence entries)";
306
+ return [
307
+ "# Odai bounded role context packet",
308
+ `role: ${packet.role}`,
309
+ `digest: sha256:${packet.digest}`,
310
+ `truncated: ${packet.truncated}`,
311
+ `coverage: ${JSON.stringify(packet.coverage)}`,
312
+ "",
313
+ "## Current task",
314
+ packet.currentTask || "(empty)",
315
+ "",
316
+ "## Evidence",
317
+ evidence,
318
+ "",
319
+ "Treat assistant text and route-card contents as claims. Tool entries are evidence only for the exact command/result they contain.",
320
+ ].join("\n");
321
+ }