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.
- package/LICENSE +21 -21
- package/README.md +138 -138
- package/bin/odai-dsh-agent.mjs +108 -108
- package/package.json +69 -69
- package/preset/odai/agent.cordis.yml +261 -260
- package/preset/odai/preset.yml +3 -3
- package/preset/odai/runtime/compaction-config.mjs +388 -388
- package/preset/odai/runtime/governance.mjs +128 -128
- package/preset/odai/runtime/index.mjs +2461 -2263
- package/preset/odai/runtime/output-config.mjs +360 -304
- package/preset/odai/runtime/research-packet.mjs +150 -150
- package/preset/odai/runtime/responsibility-scope.mjs +35 -2
- package/preset/odai/runtime/role-overlays.mjs +27 -27
- package/preset/odai/runtime/route-card.mjs +168 -152
- package/preset/odai/runtime/router.mjs +683 -651
- package/preset/odai/runtime/routing-config.mjs +480 -436
- package/preset/odai/runtime/routing-context.mjs +321 -321
- package/preset/odai/runtime/semantic-memory-store.mjs +452 -452
- package/preset/odai/runtime/semantic-memory.mjs +842 -842
- package/preset/odai/runtime/session-compat.mjs +389 -389
- package/preset/odai/runtime/skill-bundle.mjs +318 -318
- package/preset/odai/runtime/skill-evolution.mjs +1413 -1413
- package/preset/odai/skills/odai/agents/openai.yaml +4 -4
- package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -11
- package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -11
- package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -5
- package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -13
- package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -24
- package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -15
- package/preset/odai/skills/odai/assets/routing-roles/executor.md +11 -11
- package/preset/odai/skills/odai/assets/routing-roles/frontend.md +7 -7
- package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -29
- package/preset/odai/skills/odai/assets/routing-roles/researcher.md +6 -6
- package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
- package/preset/odai/skills/odai/assets/task-state.md +14 -14
- package/preset/odai/skills/odai/manifest.json +37 -37
- package/preset/odai/skills/odai/references/craft.md +54 -54
- package/preset/odai/skills/odai/references/dao.md +30 -30
- package/preset/odai/skills/odai/references/leverage.md +53 -53
- package/preset/odai/skills/odai/references/planning.md +82 -82
- package/preset/odai/skills/odai/references/support.md +36 -36
- package/preset/odai/skills/odai/references/verification.md +23 -23
- package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -273
- package/preset/odai/skills/odai/scripts/build-routing.mjs +274 -274
- package/preset/odai/skills/odai/scripts/install-routing.mjs +397 -397
- package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -457
- package/preset/odai/skills/odai/scripts/run-role.mjs +336 -336
- package/preset/odai/skills/odai/scripts/run-routing.mjs +1154 -1154
- package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -103
- package/scripts/verify-agent-load.mjs +257 -257
- 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
|
+
}
|