@nowcrew/daemon 0.6.103-beta.1 → 0.6.104
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/dist/execution-runner.js +5 -2
- package/dist/local-executor.js +8 -1
- package/dist/runtime-observation.js +219 -0
- package/dist/runtimes/claude-observation-runner.js +55 -0
- package/dist/runtimes/claude-thread-runner.js +17 -3
- package/dist/runtimes/claude.js +5 -3
- package/dist/runtimes/codex-app-server-runner.js +16 -2
- package/dist/runtimes/deepseek-harness-runner.js +16 -0
- package/dist/runtimes/kimi-acp-runner.js +19 -0
- package/dist/runtimes/observation-context.js +18 -0
- package/dist/runtimes/observation-mapper.js +314 -0
- package/dist/runtimes/opencode-runner.js +18 -4
- package/dist/runtimes/pi-runner.js +17 -1
- package/dist/token.js +4 -1
- package/package.json +1 -1
package/dist/execution-runner.js
CHANGED
|
@@ -709,9 +709,11 @@ export async function runExecution(config, input, dependencies) {
|
|
|
709
709
|
catch { /* best-effort activity omitted when its envelope cannot fit */ }
|
|
710
710
|
},
|
|
711
711
|
} : {}),
|
|
712
|
-
...(spec.reporting.streamConsole ? {
|
|
712
|
+
...(spec.reporting.streamConsole || credential.observability ? {
|
|
713
713
|
onConsole: (chunk) => {
|
|
714
|
-
if (
|
|
714
|
+
if (!spec.reporting.streamConsole && chunk.payload?.kind !== "agent_observation")
|
|
715
|
+
return;
|
|
716
|
+
if (chunk.payload?.kind !== "agent_observation" && firstOutputTimer !== undefined) {
|
|
715
717
|
clearTimeout(firstOutputTimer);
|
|
716
718
|
firstOutputTimer = undefined;
|
|
717
719
|
}
|
|
@@ -859,6 +861,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
859
861
|
return appendAgentMemoryContext(base, recalledMemory, Math.max(Buffer.byteLength(base, "utf8"), baseSystemPromptBudget - (context.reservedPromptBytes ?? 0)));
|
|
860
862
|
};
|
|
861
863
|
const localInput = {
|
|
864
|
+
...(credential.observability ? { observability: credential.observability } : {}),
|
|
862
865
|
executionId: spec.executionId,
|
|
863
866
|
handle: spec.agent.handle,
|
|
864
867
|
channelId: spec.context.channelId,
|
package/dist/local-executor.js
CHANGED
|
@@ -46,6 +46,7 @@ import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace,
|
|
|
46
46
|
import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
|
|
47
47
|
import { reportLocalMemoryV2Deferred, resolveLocalMemoryV2Rollout } from "./local-memory/rollout.js";
|
|
48
48
|
import { localMemoryV2Environment } from "./config.js";
|
|
49
|
+
import { observationEnvironment, safeRuntimeObservation } from "./runtime-observation.js";
|
|
49
50
|
export class RuntimeRunningCallbackTimeoutError extends Error {
|
|
50
51
|
constructor(runtime, timeoutMs) {
|
|
51
52
|
super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
|
|
@@ -592,7 +593,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
592
593
|
systemPromptPath: workspace.systemPromptPath,
|
|
593
594
|
systemPrompt: effectiveSystemPrompt,
|
|
594
595
|
wakePrompt,
|
|
595
|
-
env: runtimeEnv,
|
|
596
|
+
env: observationEnvironment(runtimeEnv, input.observability),
|
|
596
597
|
effectivePermission: input.effectivePermission,
|
|
597
598
|
...(launchModel === undefined ? {} : { model: launchModel }),
|
|
598
599
|
...(launchReasoning === undefined ? {} : { reasoning: launchReasoning }),
|
|
@@ -787,6 +788,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
787
788
|
const event = parseLine(line);
|
|
788
789
|
if (!event)
|
|
789
790
|
return;
|
|
791
|
+
if (typeof event === "object" && "type" in event && event.type === "nowwork.observation") {
|
|
792
|
+
const payload = safeRuntimeObservation(event.observation, input.observability);
|
|
793
|
+
if (payload)
|
|
794
|
+
callbacks.onConsole?.({ stream: "system", text: "", payload });
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
790
797
|
nativeThreadContext?.observe(event);
|
|
791
798
|
localMemoryTelemetry?.observe(event);
|
|
792
799
|
if (isRuntimeReadyEvent(runtime.name, event))
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const OBSERVABILITY_ENV = "NOWWORK_OBSERVABILITY_POLICY";
|
|
4
|
+
export const ObservabilityPolicySchema = z.object({
|
|
5
|
+
version: z.literal(1), captureId: z.string().min(1).max(128),
|
|
6
|
+
contentMode: z.enum(["metadata", "redacted"]),
|
|
7
|
+
}).strict();
|
|
8
|
+
const Id = z.string().min(1).max(200);
|
|
9
|
+
const Count = z.number().int().nonnegative().safe();
|
|
10
|
+
export const RuntimeObservationSchema = z.object({
|
|
11
|
+
kind: z.literal("agent_observation"), version: z.literal(1), captureId: z.string().min(1).max(128),
|
|
12
|
+
sourceEventIndex: z.number().int().positive().safe(),
|
|
13
|
+
runtime: z.enum(["codex", "claude", "kimi", "hermes", "deepseek-harness", "opencode", "pi"]),
|
|
14
|
+
observationId: Id, parentObservationId: Id.optional(), nativeSessionId: Id.optional(),
|
|
15
|
+
nativeTurnId: Id.optional(), nativeCallId: Id.optional(),
|
|
16
|
+
type: z.enum(["agent", "tool", "generation", "event", "span", "retriever"]),
|
|
17
|
+
phase: z.enum(["start", "end", "event"]), name: z.string().min(1).max(200),
|
|
18
|
+
observedAt: z.string().datetime({ offset: true }), status: z.enum(["ok", "error"]).optional(),
|
|
19
|
+
input: z.string().max(8192).optional(), output: z.string().max(8192).optional(), inputBytes: Count.optional(), outputBytes: Count.optional(), model: z.string().min(1).max(200).optional(),
|
|
20
|
+
usage: z.object({ inputTokens: Count.optional(), outputTokens: Count.optional(), cacheReadTokens: Count.optional(),
|
|
21
|
+
cacheCreationTokens: Count.optional(), reasoningTokens: Count.optional(), costUsd: z.number().finite().nonnegative().optional() }).strict().optional(),
|
|
22
|
+
usageScope: z.enum(["generation", "turn", "execution"]).optional(),
|
|
23
|
+
unavailable: z.array(z.string().min(1).max(120)).max(30).optional(), truncated: z.boolean().optional(),
|
|
24
|
+
expectedCount: z.number().int().nonnegative().safe().optional(), dropped: z.number().int().nonnegative().safe().optional(),
|
|
25
|
+
}).strict().superRefine((event, ctx) => {
|
|
26
|
+
if (event.name === "capture.summary") {
|
|
27
|
+
if (event.phase !== "event" || event.type !== "event" || event.expectedCount === undefined
|
|
28
|
+
|| event.sourceEventIndex !== event.expectedCount + 1)
|
|
29
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Invalid capture summary" });
|
|
30
|
+
}
|
|
31
|
+
else if (event.expectedCount !== undefined || event.dropped !== undefined) {
|
|
32
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Only capture summary carries counts" });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
export function parseObservabilityPolicy(raw) {
|
|
36
|
+
const parsed = ObservabilityPolicySchema.safeParse(raw);
|
|
37
|
+
return parsed.success ? parsed.data : undefined;
|
|
38
|
+
}
|
|
39
|
+
export function observationPolicyFromEnvironment(env) {
|
|
40
|
+
try {
|
|
41
|
+
return parseObservabilityPolicy(JSON.parse(env[OBSERVABILITY_ENV] ?? "null"));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** 最后应用,清掉继承/用户配置中的同名变量,旧执行默认关闭。 */
|
|
48
|
+
export function observationEnvironment(env, policy) {
|
|
49
|
+
const clean = Object.fromEntries(Object.entries(env).filter(([key]) => key.toUpperCase() !== OBSERVABILITY_ENV));
|
|
50
|
+
const parsed = parseObservabilityPolicy(policy);
|
|
51
|
+
return parsed ? { ...clean, [OBSERVABILITY_ENV]: JSON.stringify(parsed) } : clean;
|
|
52
|
+
}
|
|
53
|
+
const REDACTED = "[REDACTED]";
|
|
54
|
+
// 与 Server 保持相同的边界规则;原 env/spec 不能先进入本地 journal 再由 Server 清洗。
|
|
55
|
+
const normalizeKey = (key) => key.toLowerCase().replace(/[^a-z0-9]/gu, "");
|
|
56
|
+
const forbiddenContainer = (key) => /^(?:env|envvars|runtimeenv|childenv|inheritedenv|environmentvariables|processenv|environmentplan|spec|executionspec|runtimeconfig|providerconfig)$/u.test(normalizeKey(key));
|
|
57
|
+
const sensitiveKey = (key) => /(?:apikey|accesskey(?:id)?|secretkey|secret|password|passwd|token|authorization|cookie|credential|privatekey|signature)$/u.test(normalizeKey(key))
|
|
58
|
+
|| /^(?:key|sig|session|auth|authkey|authstring)$/u.test(normalizeKey(key));
|
|
59
|
+
function clip(value, limit) {
|
|
60
|
+
const end = value.charCodeAt(limit - 1);
|
|
61
|
+
return value.slice(0, end >= 0xd800 && end <= 0xdbff ? limit - 1 : limit);
|
|
62
|
+
}
|
|
63
|
+
function cleanUrl(raw) {
|
|
64
|
+
try {
|
|
65
|
+
const url = new URL(raw);
|
|
66
|
+
if (url.username || url.password) {
|
|
67
|
+
url.username = REDACTED;
|
|
68
|
+
url.password = "";
|
|
69
|
+
}
|
|
70
|
+
for (const name of [...url.searchParams.keys()]) {
|
|
71
|
+
if (sensitiveKey(name) || /^(?:code|ticket|sessionid|sas)$/iu.test(name) || /^(?:x-amz-|x-oss-|x-goog-)/iu.test(name))
|
|
72
|
+
url.searchParams.set(name, REDACTED);
|
|
73
|
+
}
|
|
74
|
+
if (url.hash && /(?:token|secret|password|credential|key|signature)/iu.test(url.hash))
|
|
75
|
+
url.hash = REDACTED;
|
|
76
|
+
return url.toString();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return REDACTED;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function redactObservationText(value) {
|
|
83
|
+
const urls = [];
|
|
84
|
+
const text = value.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"'`]+/giu, raw => {
|
|
85
|
+
const index = urls.push(cleanUrl(raw)) - 1;
|
|
86
|
+
return `__NOWWORK_TRACE_URL_${index}__`;
|
|
87
|
+
});
|
|
88
|
+
return text
|
|
89
|
+
.replace(/-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z ]+ )?PRIVATE KEY-----/gu, REDACTED)
|
|
90
|
+
.replace(/\b(?:sk|pk)[-_]lf[-_][A-Za-z0-9_-]+\b/gu, REDACTED)
|
|
91
|
+
.replace(/\b(?:sk[-_][A-Za-z0-9_-]{6,}|gh[pousr]_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,}|npm_[A-Za-z0-9]{12,})\b/gu, REDACTED)
|
|
92
|
+
.replace(/\b(authorization\s*[:=]\s*)(?:(?:bearer|basic)\s+)?[^\r\n,;]+/giu, `$1${REDACTED}`)
|
|
93
|
+
.replace(/((?:[A-Za-z0-9_]*(?:api[_-]?key|access[_-]?key|secret|password|passwd|token|cookie|credential|signature)|\b(?:key|session))\s*["']?\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;}]+)/giu, `$1${REDACTED}`)
|
|
94
|
+
.replace(/(\/\/[^\s"'`]+\/:_auth(?:token)?\s*=)\s*[^\s"'`]+/giu, `$1${REDACTED}`)
|
|
95
|
+
.replace(/__NOWWORK_TRACE_URL_(\d+)__/gu, (_match, index) => urls[Number(index)] ?? REDACTED);
|
|
96
|
+
}
|
|
97
|
+
function safeIdentity(value) {
|
|
98
|
+
// 含敏感值的 ID 用稳定别名,不能把不同调用都改成同一个 [REDACTED]。
|
|
99
|
+
return redactObservationText(value) === value ? value : `redacted-id:${createHash("sha256").update(value).digest("hex")}`;
|
|
100
|
+
}
|
|
101
|
+
function content(value) {
|
|
102
|
+
let truncated = false;
|
|
103
|
+
let remaining = 2000;
|
|
104
|
+
const visit = (item, depth) => {
|
|
105
|
+
if (--remaining < 0 || depth > 12) {
|
|
106
|
+
truncated = true;
|
|
107
|
+
return "[TRUNCATED]";
|
|
108
|
+
}
|
|
109
|
+
if (typeof item === "string") {
|
|
110
|
+
const text = redactObservationText(item);
|
|
111
|
+
if (text.length > 8192)
|
|
112
|
+
truncated = true;
|
|
113
|
+
return clip(text, 8192);
|
|
114
|
+
}
|
|
115
|
+
if (item === null || typeof item === "number" || typeof item === "boolean")
|
|
116
|
+
return item;
|
|
117
|
+
if (Array.isArray(item)) {
|
|
118
|
+
if (item.length > 100)
|
|
119
|
+
truncated = true;
|
|
120
|
+
return item.slice(0, 100).map(child => visit(child, depth + 1));
|
|
121
|
+
}
|
|
122
|
+
if (typeof item === "object") {
|
|
123
|
+
const entries = Object.entries(item);
|
|
124
|
+
if (entries.length > 100)
|
|
125
|
+
truncated = true;
|
|
126
|
+
return Object.fromEntries(entries.slice(0, 100).map(([key, child]) => [redactObservationText(key), forbiddenContainer(key) || sensitiveKey(key)
|
|
127
|
+
|| (normalizeKey(key) === "environment" && typeof child === "object") ? REDACTED : visit(child, depth + 1)]));
|
|
128
|
+
}
|
|
129
|
+
return String(item);
|
|
130
|
+
};
|
|
131
|
+
let structured = value;
|
|
132
|
+
if (typeof value === "string" && /^[\s]*[\[{]/u.test(value)) {
|
|
133
|
+
try {
|
|
134
|
+
structured = JSON.parse(value);
|
|
135
|
+
}
|
|
136
|
+
catch { /* 不是完整JSON则走文本清洗 */ }
|
|
137
|
+
}
|
|
138
|
+
const safe = visit(structured, 0);
|
|
139
|
+
const text = typeof safe === "string" ? safe : JSON.stringify(safe);
|
|
140
|
+
return { text: clip(text, 8192), truncated: truncated || text.length > 8192 };
|
|
141
|
+
}
|
|
142
|
+
/** 校验与本地内容策略在进入现有 telemetry journal 前再执行一次。 */
|
|
143
|
+
export function safeRuntimeObservation(raw, policy) {
|
|
144
|
+
if (!policy)
|
|
145
|
+
return undefined;
|
|
146
|
+
const result = RuntimeObservationSchema.safeParse(raw);
|
|
147
|
+
if (!result.success || result.data.captureId !== policy.captureId)
|
|
148
|
+
return undefined;
|
|
149
|
+
const { input, output, ...metadata } = result.data;
|
|
150
|
+
const cleanMetadata = { ...metadata, name: redactObservationText(metadata.name),
|
|
151
|
+
...(metadata.model ? { model: redactObservationText(metadata.model) } : {}),
|
|
152
|
+
...(metadata.unavailable ? { unavailable: metadata.unavailable.map(value => clip(redactObservationText(value), 120)) } : {}),
|
|
153
|
+
...Object.fromEntries(["observationId", "parentObservationId", "nativeSessionId", "nativeTurnId", "nativeCallId"]
|
|
154
|
+
.flatMap(key => metadata[key] === undefined ? [] : [[key, safeIdentity(metadata[key])]])),
|
|
155
|
+
};
|
|
156
|
+
const inputContent = policy.contentMode === "redacted" && input !== undefined ? content(input) : undefined;
|
|
157
|
+
const outputContent = policy.contentMode === "redacted" && output !== undefined ? content(output) : undefined;
|
|
158
|
+
const safe = policy.contentMode === "metadata" ? cleanMetadata : {
|
|
159
|
+
...cleanMetadata,
|
|
160
|
+
...(inputContent ? { input: inputContent.text } : {}),
|
|
161
|
+
...(outputContent ? { output: outputContent.text } : {}),
|
|
162
|
+
...(inputContent?.truncated || outputContent?.truncated ? { truncated: true } : {}),
|
|
163
|
+
};
|
|
164
|
+
const checked = RuntimeObservationSchema.safeParse(safe);
|
|
165
|
+
return checked.success && Buffer.byteLength(JSON.stringify(checked.data)) <= 24 * 1024 ? checked.data : undefined;
|
|
166
|
+
}
|
|
167
|
+
/** 仅短命 runner 使用;不持久化、不联网、不影响业务事件流。 */
|
|
168
|
+
export function createRuntimeObservationCapture(runtime, env = process.env, emit = event => { process.stdout.write(`${JSON.stringify(event)}\n`); }) {
|
|
169
|
+
const policy = observationPolicyFromEnvironment(env);
|
|
170
|
+
let index = 0;
|
|
171
|
+
let dropped = 0;
|
|
172
|
+
let finished = false;
|
|
173
|
+
const send = (fields, summary = false) => {
|
|
174
|
+
if (!policy || finished)
|
|
175
|
+
return;
|
|
176
|
+
if (!summary && index >= 20_000) {
|
|
177
|
+
dropped += 1;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const { input, output, ...metadata } = fields;
|
|
182
|
+
const bytes = (value) => Buffer.byteLength(typeof value === "string" ? value : JSON.stringify(value));
|
|
183
|
+
const inputText = policy.contentMode === "redacted" && input !== undefined ? content(input) : undefined;
|
|
184
|
+
const outputText = policy.contentMode === "redacted" && output !== undefined ? content(output) : undefined;
|
|
185
|
+
let candidate = {
|
|
186
|
+
...metadata, kind: "agent_observation", version: 1, captureId: policy.captureId,
|
|
187
|
+
runtime, sourceEventIndex: index + 1, observedAt: fields.observedAt ?? new Date().toISOString(),
|
|
188
|
+
...(input === undefined ? {} : { inputBytes: bytes(input) }), ...(output === undefined ? {} : { outputBytes: bytes(output) }),
|
|
189
|
+
...(inputText ? { input: inputText.text } : {}), ...(outputText ? { output: outputText.text } : {}),
|
|
190
|
+
...(inputText?.truncated || outputText?.truncated ? { truncated: true } : {}),
|
|
191
|
+
};
|
|
192
|
+
if (Buffer.byteLength(JSON.stringify(candidate)) > 24 * 1024) {
|
|
193
|
+
candidate = { ...candidate, ...(inputText ? { input: clip(inputText.text, 1500) } : {}),
|
|
194
|
+
...(outputText ? { output: clip(outputText.text, 1500) } : {}), truncated: true };
|
|
195
|
+
}
|
|
196
|
+
const safe = safeRuntimeObservation(candidate, policy);
|
|
197
|
+
if (!safe) {
|
|
198
|
+
dropped += 1;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
index += 1;
|
|
202
|
+
emit({ type: "nowwork.observation", observation: safe });
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
dropped += 1; /* 诊断永远不改变 runtime 结果 */
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
enabled: policy !== undefined,
|
|
210
|
+
observe: fields => send(fields),
|
|
211
|
+
finish() {
|
|
212
|
+
if (finished)
|
|
213
|
+
return;
|
|
214
|
+
send({ observationId: "capture.summary", type: "event", phase: "event", name: "capture.summary",
|
|
215
|
+
expectedCount: index, dropped, ...(index === 0 ? { unavailable: ["runtime_events"] } : {}) }, true);
|
|
216
|
+
finished = true;
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** 无线程 Claude 的短命观察桥:保留原命令/参数,不引入线程或原生会话约束。 */
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import spawn from "cross-spawn";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createRuntimeObservationCapture } from "../runtime-observation.js";
|
|
7
|
+
import { createObservationMapper } from "./observation-mapper.js";
|
|
8
|
+
import { observeClaudeContext } from "./observation-context.js";
|
|
9
|
+
const Input = z.object({ bin: z.string().min(1), args: z.array(z.string()) }).strict();
|
|
10
|
+
export async function runClaudeObservation(bin, args) {
|
|
11
|
+
const capture = createRuntimeObservationCapture("claude");
|
|
12
|
+
const observer = createObservationMapper("claude", capture);
|
|
13
|
+
await observeClaudeContext(observer, args, args.at(-1) ?? "");
|
|
14
|
+
const child = spawn(bin, [...args], { cwd: process.cwd(), env: process.env, stdio: ["ignore", "pipe", "pipe"], detached: false });
|
|
15
|
+
const stopped = new Promise((resolve, reject) => {
|
|
16
|
+
child.once("error", reject);
|
|
17
|
+
child.once("close", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
|
|
18
|
+
});
|
|
19
|
+
void stopped.catch(() => undefined);
|
|
20
|
+
const cancel = () => { child.kill("SIGTERM"); };
|
|
21
|
+
process.once("SIGTERM", cancel);
|
|
22
|
+
process.once("SIGINT", cancel);
|
|
23
|
+
try {
|
|
24
|
+
if (!child.stdout || !child.stderr)
|
|
25
|
+
throw new Error("Claude observation runner requires output streams");
|
|
26
|
+
child.stderr.pipe(process.stderr, { end: false });
|
|
27
|
+
for await (const line of createInterface({ input: child.stdout })) {
|
|
28
|
+
try {
|
|
29
|
+
observer.observe(JSON.parse(line));
|
|
30
|
+
}
|
|
31
|
+
catch { /* 原非 JSON 输出仍透传 */ }
|
|
32
|
+
process.stdout.write(`${line}\n`);
|
|
33
|
+
}
|
|
34
|
+
return await stopped;
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
observer.finish();
|
|
38
|
+
capture.finish();
|
|
39
|
+
process.off("SIGTERM", cancel);
|
|
40
|
+
process.off("SIGINT", cancel);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
44
|
+
let raw = "";
|
|
45
|
+
for await (const chunk of process.stdin) {
|
|
46
|
+
raw += String(chunk);
|
|
47
|
+
if (Buffer.byteLength(raw) > 8 * 1024 * 1024)
|
|
48
|
+
throw new Error("Claude observation input too large");
|
|
49
|
+
}
|
|
50
|
+
const input = Input.parse(JSON.parse(raw));
|
|
51
|
+
runClaudeObservation(input.bin, input.args).then(code => { process.exitCode = code; }).catch(() => {
|
|
52
|
+
process.stderr.write("Claude observation runner failed\n");
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -8,6 +8,9 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
8
8
|
import spawn from "cross-spawn";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { prepareThreadDelivery, ThreadDeliveryColdStartRequired } from "../thread-input-delivery.js";
|
|
11
|
+
import { createRuntimeObservationCapture } from "../runtime-observation.js";
|
|
12
|
+
import { createObservationMapper } from "./observation-mapper.js";
|
|
13
|
+
import { observeClaudeContext } from "./observation-context.js";
|
|
11
14
|
const InputSchema = z.object({ bin: z.string().min(1), args: z.array(z.string()),
|
|
12
15
|
wakePrompt: z.string().min(1), sessionId: z.string().min(1).optional(), resume: z.boolean() }).strict();
|
|
13
16
|
class PreflightError extends Error {
|
|
@@ -52,7 +55,7 @@ async function bounded(promise, milliseconds, label) {
|
|
|
52
55
|
clearTimeout(timer);
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
|
-
async function attempt(input, sessionId, resume, options) {
|
|
58
|
+
async function attempt(input, sessionId, resume, options, observer) {
|
|
56
59
|
const proofDir = await mkdtemp(join(tmpdir(), "nowwork-claude-session-"));
|
|
57
60
|
const proofPath = join(proofDir, "proof.json");
|
|
58
61
|
const nonce = randomUUID();
|
|
@@ -182,6 +185,7 @@ async function attempt(input, sessionId, resume, options) {
|
|
|
182
185
|
// work is outstanding; background shells must not hold it open forever.
|
|
183
186
|
if (event.type === "result")
|
|
184
187
|
sawResult = pendingAgents.size === 0;
|
|
188
|
+
observer.observe(event);
|
|
185
189
|
output(event);
|
|
186
190
|
}).catch(error => { fail(error instanceof Error ? error : new Error("claude_event_failed")); void stop().catch(fail); });
|
|
187
191
|
});
|
|
@@ -207,6 +211,7 @@ async function attempt(input, sessionId, resume, options) {
|
|
|
207
211
|
if (resume)
|
|
208
212
|
output({ type: "nowwork.native_resume", runtime: "claude", session_id: sessionId });
|
|
209
213
|
submitted = true;
|
|
214
|
+
await observeClaudeContext(observer, input.args, delivery.prompt);
|
|
210
215
|
await send({ type: "user", session_id: "", uuid: userId, parent_tool_use_id: null,
|
|
211
216
|
message: { role: "user", content: delivery.prompt } });
|
|
212
217
|
await bounded(accepted.promise, options.ackTimeoutMs ?? 30_000, "claude_user_message_not_acknowledged");
|
|
@@ -237,16 +242,25 @@ async function attempt(input, sessionId, resume, options) {
|
|
|
237
242
|
}
|
|
238
243
|
}
|
|
239
244
|
export async function runClaudeThread(input, options = {}) {
|
|
245
|
+
const capture = createRuntimeObservationCapture("claude", options.env ?? process.env, options.output);
|
|
246
|
+
try {
|
|
247
|
+
return await runClaudeObserved(input, options, capture);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
capture.finish();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function runClaudeObserved(input, options, capture) {
|
|
240
254
|
const sessionId = input.sessionId ?? randomUUID();
|
|
241
255
|
try {
|
|
242
|
-
return await attempt(input, sessionId, input.resume, options);
|
|
256
|
+
return await attempt(input, sessionId, input.resume, options, createObservationMapper("claude", capture, "attempt:1"));
|
|
243
257
|
}
|
|
244
258
|
catch (error) {
|
|
245
259
|
// The failed attempt never received a business user turn or entered its
|
|
246
260
|
// model/tool loop. Native startup hooks may run again on cold recovery.
|
|
247
261
|
if (!input.resume || !(error instanceof PreflightError || error instanceof ThreadDeliveryColdStartRequired))
|
|
248
262
|
throw error;
|
|
249
|
-
return attempt(input, randomUUID(), false, options);
|
|
263
|
+
return attempt(input, randomUUID(), false, options, createObservationMapper("claude", capture, "attempt:2"));
|
|
250
264
|
}
|
|
251
265
|
}
|
|
252
266
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
package/dist/runtimes/claude.js
CHANGED
|
@@ -7,6 +7,7 @@ import { posix, win32 } from "node:path";
|
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
8
|
// cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
|
|
9
9
|
import spawn from "cross-spawn";
|
|
10
|
+
import { observationPolicyFromEnvironment } from "../runtime-observation.js";
|
|
10
11
|
// Claude Code 原生 --effort 档位(claude 2.1.196 实测:--help 与非法值告警均枚举这五档)。
|
|
11
12
|
// 白名单外的值(含 "default" 与 codex 专属档)回落 CLAUDE_DEFAULT_EFFORT,脏数据不影响启动。
|
|
12
13
|
export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
@@ -280,14 +281,15 @@ export function buildClaudeArgs(input) {
|
|
|
280
281
|
}
|
|
281
282
|
export function buildClaudeLaunch(input) {
|
|
282
283
|
const args = buildClaudeArgs(input);
|
|
283
|
-
|
|
284
|
+
const thread = Boolean(input.env.CREW_THREAD_ROOT?.trim());
|
|
285
|
+
if (!thread && !observationPolicyFromEnvironment(input.env))
|
|
284
286
|
return { command: input.bin, args, cwd: input.cwd, env: input.env };
|
|
285
287
|
const source = import.meta.url.endsWith(".ts");
|
|
286
288
|
return { command: process.execPath,
|
|
287
289
|
args: [...(source ? ["--import", pathToFileURL(createRequire(import.meta.url).resolve("tsx/esm")).href] : []),
|
|
288
|
-
fileURLToPath(new URL(
|
|
290
|
+
fileURLToPath(new URL(`./${thread ? "claude-thread-runner" : "claude-observation-runner"}.${source ? "ts" : "js"}`, import.meta.url))],
|
|
289
291
|
cwd: input.cwd, env: input.env,
|
|
290
|
-
stdinText: JSON.stringify({ bin: input.bin, args: args.slice(0, -1), wakePrompt: input.wakePrompt,
|
|
292
|
+
stdinText: JSON.stringify(!thread ? { bin: input.bin, args } : { bin: input.bin, args: args.slice(0, -1), wakePrompt: input.wakePrompt,
|
|
291
293
|
...(input.sessionId ? { sessionId: input.sessionId } : {}), resume: input.resume === true }) };
|
|
292
294
|
}
|
|
293
295
|
export function spawnClaude(input) {
|
|
@@ -11,6 +11,8 @@ import { decideTurnRetry, itemMakesTurnUnsafe } from "./codex-turn-retry.js";
|
|
|
11
11
|
import { startFirstProgressWatchdog } from "./progress-watchdog.js";
|
|
12
12
|
import { prepareThreadDelivery, ThreadDeliveryColdStartRequired } from "../thread-input-delivery.js";
|
|
13
13
|
import { codexExecutionPolicyArgs } from "./codex-execution-policy.js";
|
|
14
|
+
import { createRuntimeObservationCapture } from "../runtime-observation.js";
|
|
15
|
+
import { createObservationMapper } from "./observation-mapper.js";
|
|
14
16
|
const RunnerInputSchema = z.object({
|
|
15
17
|
systemPrompt: z.string().min(1),
|
|
16
18
|
wakePrompt: z.string().min(1),
|
|
@@ -443,7 +445,8 @@ async function stopChildTree(child) {
|
|
|
443
445
|
throw new Error(`Codex app-server process tree ${child.pid ?? "unknown"} did not exit`);
|
|
444
446
|
}
|
|
445
447
|
}
|
|
446
|
-
async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs, resumeEventSessionId) {
|
|
448
|
+
async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs, resumeEventSessionId, capture) {
|
|
449
|
+
const observer = createObservationMapper("codex", capture, `attempt:${attempt}`);
|
|
447
450
|
const attemptStartedAt = Date.now();
|
|
448
451
|
const child = spawn(bin, codexAppServerArgs({
|
|
449
452
|
...(input.projectRootMarkers === undefined ? {} : { projectRootMarkers: input.projectRootMarkers }),
|
|
@@ -483,6 +486,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
483
486
|
let firstProgress = startFirstProgressWatchdog(() => undefined);
|
|
484
487
|
firstProgress.stop();
|
|
485
488
|
const rpc = new CodexRpcClient(child, input.effectivePermission, async (method, params) => {
|
|
489
|
+
observer.observe(params, method);
|
|
486
490
|
if (method === "thread/tokenUsage/updated") {
|
|
487
491
|
lastUsage = params.tokenUsage;
|
|
488
492
|
}
|
|
@@ -625,6 +629,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
625
629
|
});
|
|
626
630
|
activeStage = "model_init";
|
|
627
631
|
stageStartedAt = Date.now();
|
|
632
|
+
observer.context({ systemPrompt: input.systemPrompt, prompt: delivery.prompt });
|
|
628
633
|
const started = await rpc.request("turn/start", {
|
|
629
634
|
threadId,
|
|
630
635
|
input: [
|
|
@@ -712,6 +717,15 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
712
717
|
}
|
|
713
718
|
}
|
|
714
719
|
export async function runCodexAppServer(bin, options = {}) {
|
|
720
|
+
const capture = createRuntimeObservationCapture("codex");
|
|
721
|
+
try {
|
|
722
|
+
return await runCodexObserved(bin, options, capture);
|
|
723
|
+
}
|
|
724
|
+
finally {
|
|
725
|
+
capture.finish();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async function runCodexObserved(bin, options, capture) {
|
|
715
729
|
const input = await readRunnerInput();
|
|
716
730
|
const initializeTimeoutMs = Math.min(options.initializeTimeoutMs ?? INITIALIZE_RPC_TIMEOUT_MS, INITIALIZE_RPC_TIMEOUT_MS);
|
|
717
731
|
const turnRetryDelayMs = options.turnRetryDelayMs ?? TRANSIENT_TURN_RETRY_DELAY_MS;
|
|
@@ -721,7 +735,7 @@ export async function runCodexAppServer(bin, options = {}) {
|
|
|
721
735
|
let attemptInput = input;
|
|
722
736
|
const resumeEventSessionId = input.resume ? input.sessionId : undefined;
|
|
723
737
|
for (let attempt = 1;; attempt += 1) {
|
|
724
|
-
const result = await runCodexAppServerAttempt(bin, attemptInput, attempt, initializeTimeoutMs, resumeEventSessionId);
|
|
738
|
+
const result = await runCodexAppServerAttempt(bin, attemptInput, attempt, initializeTimeoutMs, resumeEventSessionId, capture);
|
|
725
739
|
if (result.restartCold) {
|
|
726
740
|
if (coldRestarted)
|
|
727
741
|
return result.code;
|
|
@@ -11,6 +11,8 @@ import { deepSeekHarnessLaunch } from "./deepseek-harness-launch.js";
|
|
|
11
11
|
import { prepareHarnessProfile } from "./deepseek-harness-profile.js";
|
|
12
12
|
import { mapAcpUpdate } from "./kimi-acp-runner.js";
|
|
13
13
|
import { prepareThreadDelivery } from "../thread-input-delivery.js";
|
|
14
|
+
import { createRuntimeObservationCapture } from "../runtime-observation.js";
|
|
15
|
+
import { createObservationMapper } from "./observation-mapper.js";
|
|
14
16
|
const absolutePath = z.string().refine(isAbsolute, "must be an absolute path");
|
|
15
17
|
export const HarnessInputSchema = z
|
|
16
18
|
.object({
|
|
@@ -45,6 +47,17 @@ async function settleWithin(promise, ms) {
|
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
export async function runHarness(input, bin, patchPath) {
|
|
50
|
+
const capture = createRuntimeObservationCapture("deepseek-harness");
|
|
51
|
+
const observer = createObservationMapper("deepseek-harness", capture);
|
|
52
|
+
try {
|
|
53
|
+
return await runHarnessObserved(input, bin, patchPath, observer);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
observer.finish();
|
|
57
|
+
capture.finish();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function runHarnessObserved(input, bin, patchPath, observer) {
|
|
48
61
|
const delivery = await prepareThreadDelivery({
|
|
49
62
|
env: process.env,
|
|
50
63
|
resumed: false,
|
|
@@ -115,6 +128,7 @@ export async function runHarness(input, bin, patchPath) {
|
|
|
115
128
|
.onNotification(methods.client.session.update, async ({ params }) => {
|
|
116
129
|
if (params.sessionId !== sessionId)
|
|
117
130
|
return;
|
|
131
|
+
observer.observe(params);
|
|
118
132
|
for (const event of mapAcpUpdate("deepseek-harness", params.update))
|
|
119
133
|
await emit(event);
|
|
120
134
|
});
|
|
@@ -141,6 +155,7 @@ export async function runHarness(input, bin, patchPath) {
|
|
|
141
155
|
throw new Error("Harness session unavailable");
|
|
142
156
|
await emit({ type: "thread.started", thread_id: sessionId });
|
|
143
157
|
stage = "prompt";
|
|
158
|
+
observer.context({ systemPrompt: input.systemPrompt, prompt: delivery.prompt });
|
|
144
159
|
const response = await ctx.request(methods.agent.session.prompt, {
|
|
145
160
|
sessionId,
|
|
146
161
|
prompt: [{ type: "text", text: delivery.prompt }],
|
|
@@ -155,6 +170,7 @@ export async function runHarness(input, bin, patchPath) {
|
|
|
155
170
|
}
|
|
156
171
|
return response;
|
|
157
172
|
});
|
|
173
|
+
observer.observe({ type: "nowwork.acp.result", sessionId, ...result });
|
|
158
174
|
const usage = result.usage;
|
|
159
175
|
if (!cancelled)
|
|
160
176
|
await emit({
|
|
@@ -12,6 +12,8 @@ import { augmentedPath } from "../runtime-path.js";
|
|
|
12
12
|
import { prepareThreadDelivery } from "../thread-input-delivery.js";
|
|
13
13
|
import { startFirstProgressWatchdog } from "./progress-watchdog.js";
|
|
14
14
|
import { assertKimiLegacyPromptFits, buildKimiArgs } from "./kimi.js";
|
|
15
|
+
import { createRuntimeObservationCapture } from "../runtime-observation.js";
|
|
16
|
+
import { createObservationMapper } from "./observation-mapper.js";
|
|
15
17
|
const ERROR_MESSAGE_CAP = 2_000;
|
|
16
18
|
// macOS launchd cold-starts ACP runtimes materially slower than an interactive
|
|
17
19
|
// shell. DeepSeek Harness was measured at ~4.4s before returning initialize
|
|
@@ -322,6 +324,18 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
|
322
324
|
}
|
|
323
325
|
}
|
|
324
326
|
export async function runKimiAcp(requestedOptions) {
|
|
327
|
+
const provider = requestedOptions.provider ?? "kimi";
|
|
328
|
+
const capture = createRuntimeObservationCapture(provider);
|
|
329
|
+
const observer = createObservationMapper(provider, capture);
|
|
330
|
+
try {
|
|
331
|
+
return await runKimiObserved(requestedOptions, observer, capture);
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
observer.finish();
|
|
335
|
+
capture.finish();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function runKimiObserved(requestedOptions, observer, capture) {
|
|
325
339
|
// Until this adapter verifies native restoration, thread-bound runs are cold.
|
|
326
340
|
const options = process.env.CREW_THREAD_ROOT ? { ...requestedOptions, resume: false } : requestedOptions;
|
|
327
341
|
const provider = options.provider ?? "kimi";
|
|
@@ -364,6 +378,7 @@ export async function runKimiAcp(requestedOptions) {
|
|
|
364
378
|
const app = client({ name: `nowcrew-daemon-${provider}` })
|
|
365
379
|
.onRequest(methods.client.session.requestPermission, ({ params }) => selectKimiPermission(params, permission))
|
|
366
380
|
.onNotification(methods.client.session.update, async ({ params }) => {
|
|
381
|
+
observer.observe(params);
|
|
367
382
|
acpSemanticProgress = true;
|
|
368
383
|
firstProgress.observe();
|
|
369
384
|
for (const event of mapAcpUpdate(provider, params.update))
|
|
@@ -433,6 +448,7 @@ export async function runKimiAcp(requestedOptions) {
|
|
|
433
448
|
progressTimedOut = true;
|
|
434
449
|
void cancel();
|
|
435
450
|
});
|
|
451
|
+
observer.context({ prompt });
|
|
436
452
|
return nextContext.request(methods.agent.session.prompt, {
|
|
437
453
|
sessionId,
|
|
438
454
|
prompt: [{ type: "text", text: prompt }],
|
|
@@ -443,6 +459,7 @@ export async function runKimiAcp(requestedOptions) {
|
|
|
443
459
|
process.stderr.write(`${displayName} produced no semantic progress within the startup window\n`);
|
|
444
460
|
return 1;
|
|
445
461
|
}
|
|
462
|
+
observer.observe({ type: "nowwork.acp.result", sessionId, ...result });
|
|
446
463
|
const usage = result.usage;
|
|
447
464
|
await jsonLine({
|
|
448
465
|
type: "turn.completed",
|
|
@@ -464,6 +481,8 @@ export async function runKimiAcp(requestedOptions) {
|
|
|
464
481
|
return 1;
|
|
465
482
|
}
|
|
466
483
|
if (provider === "kimi" && !acpSemanticProgress && isKimiAuthenticationRequired(error)) {
|
|
484
|
+
capture.observe({ observationId: "fallback", type: "event", phase: "event", name: "capture.unsupported",
|
|
485
|
+
unavailable: ["kimi_legacy_fallback_events"] });
|
|
467
486
|
firstProgress.stop();
|
|
468
487
|
await stopChild(child);
|
|
469
488
|
process.stderr.write("Kimi ACP requires account login; falling back to configured CLI provider transport\n");
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
/** 只观察实际发给 runtime 的 prompt;不读取配置、环境变量或凭据文件。 */
|
|
3
|
+
export async function observeClaudeContext(observer, args, prompt) {
|
|
4
|
+
if (!observer.enabled)
|
|
5
|
+
return;
|
|
6
|
+
const at = args.indexOf("--append-system-prompt-file");
|
|
7
|
+
const path = at >= 0 ? args[at + 1] : undefined;
|
|
8
|
+
if (!path) {
|
|
9
|
+
observer.context({ prompt }, ["system_prompt"]);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
observer.context({ systemPrompt: await readFile(path, "utf8"), prompt });
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
observer.context({ prompt }, ["system_prompt"]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const record = (raw) => raw !== null && typeof raw === "object" ? raw : {};
|
|
3
|
+
const string = (raw) => typeof raw === "string" && raw.length > 0 ? raw : undefined;
|
|
4
|
+
const list = (raw) => Array.isArray(raw) ? raw : [];
|
|
5
|
+
const boundedId = (raw) => {
|
|
6
|
+
const value = string(raw);
|
|
7
|
+
return value && value.length <= 150 ? value : undefined;
|
|
8
|
+
};
|
|
9
|
+
function usage(raw) {
|
|
10
|
+
const value = record(raw);
|
|
11
|
+
const pairs = {
|
|
12
|
+
inputTokens: value.input_tokens ?? value.inputTokens ?? value.input,
|
|
13
|
+
outputTokens: value.output_tokens ?? value.outputTokens ?? value.output,
|
|
14
|
+
cacheReadTokens: value.cache_read_input_tokens ?? value.cachedInputTokens ?? value.cachedReadTokens ?? value.cacheRead ?? record(value.cache).read,
|
|
15
|
+
cacheCreationTokens: value.cache_creation_input_tokens ?? value.cachedWriteTokens ?? value.cacheWrite ?? record(value.cache).write,
|
|
16
|
+
reasoningTokens: value.reasoning_tokens ?? value.reasoningTokens ?? value.reasoning,
|
|
17
|
+
costUsd: value.total_cost_usd ?? value.costUsd ?? record(value.cost).total,
|
|
18
|
+
};
|
|
19
|
+
const entries = Object.entries(pairs).filter(([, v]) => typeof v === "number" && Number.isFinite(v) && v >= 0);
|
|
20
|
+
return entries.length ? Object.fromEntries(entries) : undefined;
|
|
21
|
+
}
|
|
22
|
+
const errorStatus = (raw) => ["failed", "error", "cancelled", "interrupted", "aborted"].includes(String(raw)) ? "error" : "ok";
|
|
23
|
+
/** 原始事件的轻量投影;状态仅属于一次短命 runtime attempt,不从日志邻接猜父子关系。 */
|
|
24
|
+
export function createObservationMapper(runtime, capture, prefix = "run") {
|
|
25
|
+
let eventIndex = 0;
|
|
26
|
+
let nativeSessionId;
|
|
27
|
+
let nativeTurnId;
|
|
28
|
+
let textBuffer = "";
|
|
29
|
+
let thoughtBuffer = "";
|
|
30
|
+
let bufferedText = false;
|
|
31
|
+
let bufferedThought = false;
|
|
32
|
+
let bufferTruncated = false;
|
|
33
|
+
const started = new Map();
|
|
34
|
+
const emit = (fields) => capture.observe({
|
|
35
|
+
...(nativeSessionId ? { nativeSessionId } : {}), ...(nativeTurnId ? { nativeTurnId } : {}), ...fields,
|
|
36
|
+
});
|
|
37
|
+
const id = (native) => `${prefix}:${native}`.slice(0, 200);
|
|
38
|
+
const event = (name, fields = {}) => {
|
|
39
|
+
emit({ observationId: id(`event:${++eventIndex}`), type: "event", phase: "event", name, ...fields });
|
|
40
|
+
};
|
|
41
|
+
const tool = (native, phase, name, fields = {}) => {
|
|
42
|
+
const callId = boundedId(native);
|
|
43
|
+
const key = callId ? id(`tool:${callId}`) : undefined;
|
|
44
|
+
const knownName = key ? started.get(key) : undefined;
|
|
45
|
+
// pending/running 等状态更新不重置开始时间,缓存仅限一次短命 attempt。
|
|
46
|
+
if (phase === "start" && knownName !== undefined)
|
|
47
|
+
return;
|
|
48
|
+
if (key && phase === "start" && started.size < 20_000)
|
|
49
|
+
started.set(key, name);
|
|
50
|
+
const hasStart = key !== undefined && started.has(key);
|
|
51
|
+
emit({ observationId: key ?? id(`tool-unpaired:${++eventIndex}`), type: "tool", phase: key ? phase : "event",
|
|
52
|
+
name: phase === "end" && knownName !== undefined ? knownName : name,
|
|
53
|
+
...(callId ? { nativeCallId: callId } : {}),
|
|
54
|
+
...(!callId ? { unavailable: ["native_call_id", "paired_duration"] }
|
|
55
|
+
: phase === "end" && !hasStart ? { unavailable: ["start_time"] } : {}), ...fields });
|
|
56
|
+
};
|
|
57
|
+
const flush = () => {
|
|
58
|
+
if (bufferedText)
|
|
59
|
+
event("model.output", { output: textBuffer, ...(bufferTruncated ? { truncated: true } : {}) });
|
|
60
|
+
if (bufferedThought)
|
|
61
|
+
event("model.reasoning", { output: thoughtBuffer, ...(bufferTruncated ? { truncated: true } : {}) });
|
|
62
|
+
textBuffer = "";
|
|
63
|
+
thoughtBuffer = "";
|
|
64
|
+
bufferedText = false;
|
|
65
|
+
bufferedThought = false;
|
|
66
|
+
bufferTruncated = false;
|
|
67
|
+
};
|
|
68
|
+
const buffer = (value, thought) => {
|
|
69
|
+
const text = typeof value === "string" ? value : "";
|
|
70
|
+
if (thought) {
|
|
71
|
+
bufferedThought = true;
|
|
72
|
+
thoughtBuffer += text;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
bufferedText = true;
|
|
76
|
+
textBuffer += text;
|
|
77
|
+
}
|
|
78
|
+
if (textBuffer.length > 8192 || thoughtBuffer.length > 8192)
|
|
79
|
+
bufferTruncated = true;
|
|
80
|
+
const clip = (value) => value.slice(0, value.charCodeAt(8191) >= 0xd800 && value.charCodeAt(8191) <= 0xdbff ? 8191 : 8192);
|
|
81
|
+
textBuffer = clip(textBuffer);
|
|
82
|
+
thoughtBuffer = clip(thoughtBuffer);
|
|
83
|
+
};
|
|
84
|
+
const totals = (raw, scope, name = "runtime.usage") => {
|
|
85
|
+
const measured = usage(raw);
|
|
86
|
+
if (measured)
|
|
87
|
+
event(name, { usage: measured, usageScope: scope });
|
|
88
|
+
};
|
|
89
|
+
const codex = (method, raw) => {
|
|
90
|
+
nativeSessionId = boundedId(raw.threadId) ?? boundedId(record(raw.thread).id) ?? nativeSessionId;
|
|
91
|
+
nativeTurnId = boundedId(raw.turnId) ?? boundedId(record(raw.turn).id) ?? nativeTurnId;
|
|
92
|
+
if (method === "thread/tokenUsage/updated") {
|
|
93
|
+
const explicitTurn = boundedId(raw.turnId);
|
|
94
|
+
const explicitSession = boundedId(raw.threadId);
|
|
95
|
+
for (const source of ["last", "total"]) {
|
|
96
|
+
const measured = usage(record(raw.tokenUsage)[source]);
|
|
97
|
+
if (!measured)
|
|
98
|
+
continue;
|
|
99
|
+
const owner = source === "last" && explicitTurn ? `turn:${explicitTurn}`
|
|
100
|
+
: explicitSession ? `thread:${explicitSession}` : undefined;
|
|
101
|
+
event(`runtime.usage.${source}`, { usage: measured,
|
|
102
|
+
...(owner ? { observationId: id(`usage:${source}:${owner}`) } : {}),
|
|
103
|
+
...(source === "last" && explicitTurn ? { parentObservationId: id(`turn:${explicitTurn}`) } : {}),
|
|
104
|
+
nativeTurnId: explicitTurn,
|
|
105
|
+
unavailable: ["usage_scope", "provider_request_id", ...(owner ? [] : ["native_usage_owner"])],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (method === "turn/started" || method === "turn/completed") {
|
|
111
|
+
const turn = record(raw.turn);
|
|
112
|
+
emit({ observationId: id(`turn:${nativeTurnId ?? ++eventIndex}`), type: "agent",
|
|
113
|
+
phase: method === "turn/started" ? "start" : "end", name: "agent.turn",
|
|
114
|
+
...(method === "turn/completed" ? { status: errorStatus(turn.status), output: record(turn.error).message } : {}),
|
|
115
|
+
unavailable: ["provider_request_id", "per_generation_input", "per_generation_usage"] });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (method !== "item/started" && method !== "item/completed")
|
|
119
|
+
return;
|
|
120
|
+
const item = record(raw.item);
|
|
121
|
+
const itemType = String(item.type ?? "unknown");
|
|
122
|
+
const phase = method === "item/started" ? "start" : "end";
|
|
123
|
+
const explicitTurn = boundedId(raw.turnId);
|
|
124
|
+
const turnParent = explicitTurn ? { parentObservationId: id(`turn:${explicitTurn}`) } : {};
|
|
125
|
+
if (["commandExecution", "mcpToolCall", "dynamicToolCall", "fileChange", "webSearch", "collabAgentToolCall"].includes(itemType)) {
|
|
126
|
+
const native = boundedId(item.id);
|
|
127
|
+
const name = itemType === "mcpToolCall" ? `mcp.${String(item.tool ?? "tool")}` : itemType;
|
|
128
|
+
tool(native, phase, name.slice(0, 200), {
|
|
129
|
+
...turnParent,
|
|
130
|
+
...(phase === "start" ? { input: item.arguments ?? item.command ?? item.prompt ?? item.changes } : {
|
|
131
|
+
output: item.result ?? item.aggregatedOutput ?? item.error ?? item.agentsStates ?? item.changes,
|
|
132
|
+
status: itemType === "commandExecution" && typeof item.exitCode === "number" && item.exitCode !== 0
|
|
133
|
+
? "error" : errorStatus(item.status),
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
if (itemType === "collabAgentToolCall" && phase === "end") {
|
|
137
|
+
for (const childId of list(item.receiverThreadIds)) {
|
|
138
|
+
const child = boundedId(childId);
|
|
139
|
+
if (child && native)
|
|
140
|
+
emit({ observationId: id(`child:${createHash("sha256").update(JSON.stringify([native, child])).digest("hex")}`), parentObservationId: id(`tool:${native}`),
|
|
141
|
+
type: "agent", phase: "event", name: "agent.delegated", nativeSessionId: child,
|
|
142
|
+
unavailable: ["child_internal_events", "child_start_time", "child_end_time"] });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (phase !== "end")
|
|
148
|
+
return;
|
|
149
|
+
const nativeItem = boundedId(item.id);
|
|
150
|
+
const itemIdentity = { ...turnParent, ...(nativeItem ? { observationId: id(`item:${nativeItem}`) } : {}) };
|
|
151
|
+
if (itemType === "agentMessage")
|
|
152
|
+
event("model.output", { output: item.text, ...itemIdentity });
|
|
153
|
+
else if (itemType === "reasoning")
|
|
154
|
+
event("model.reasoning", {
|
|
155
|
+
output: item.summary ?? item.content ?? item.text, ...itemIdentity,
|
|
156
|
+
...(!item.summary && !item.content && !item.text ? { unavailable: ["reasoning_content"] } : {}),
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
const claude = (raw) => {
|
|
160
|
+
nativeSessionId = boundedId(raw.session_id) ?? nativeSessionId;
|
|
161
|
+
const message = record(raw.message);
|
|
162
|
+
const messageId = boundedId(message.id) ?? boundedId(raw.uuid);
|
|
163
|
+
const parent = boundedId(raw.parent_tool_use_id);
|
|
164
|
+
const parentFields = parent ? { parentObservationId: id(`tool:${parent}`) } : {};
|
|
165
|
+
if (raw.type === "assistant") {
|
|
166
|
+
for (const [blockIndex, value] of list(message.content).entries()) {
|
|
167
|
+
const block = record(value);
|
|
168
|
+
const identity = messageId ? { observationId: id(`message:${messageId}:block:${blockIndex}`) } : {};
|
|
169
|
+
if (block.type === "tool_use")
|
|
170
|
+
tool(block.id, "start", String(block.name ?? "tool").slice(0, 200), { input: block.input, ...parentFields });
|
|
171
|
+
if (block.type === "text")
|
|
172
|
+
event("model.output", { output: block.text, ...parentFields, ...identity });
|
|
173
|
+
if (block.type === "thinking" || block.type === "redacted_thinking")
|
|
174
|
+
event("model.reasoning", {
|
|
175
|
+
...(string(block.thinking) ? { output: block.thinking } : { unavailable: ["reasoning_content"] }), ...parentFields, ...identity,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const measured = usage(message.usage);
|
|
179
|
+
if (measured)
|
|
180
|
+
event("model.response", { usage: measured, usageScope: "generation",
|
|
181
|
+
...(messageId ? { observationId: id(`message:${messageId}:usage`) } : {}),
|
|
182
|
+
...(string(message.model) ? { model: String(message.model) } : {}),
|
|
183
|
+
unavailable: ["request_start_time", "provider_request_id", ...(messageId ? [] : ["native_message_id"])], ...parentFields });
|
|
184
|
+
}
|
|
185
|
+
else if (raw.type === "user") {
|
|
186
|
+
for (const value of list(message.content)) {
|
|
187
|
+
const block = record(value);
|
|
188
|
+
if (block.type === "tool_result")
|
|
189
|
+
tool(block.tool_use_id, "end", "tool.result", {
|
|
190
|
+
output: block.content, status: block.is_error === true ? "error" : "ok", ...parentFields,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (raw.type === "system" && boundedId(raw.task_id)) {
|
|
195
|
+
const task = String(raw.task_id);
|
|
196
|
+
const callId = boundedId(raw.tool_use_id);
|
|
197
|
+
emit({ observationId: id(`task:${task}`), type: "agent", name: "agent.delegated",
|
|
198
|
+
phase: raw.subtype === "task_started" ? "start" : raw.subtype === "task_notification" ? "end" : "event",
|
|
199
|
+
...(callId ? { parentObservationId: id(`tool:${callId}`) } : {}), output: raw.summary,
|
|
200
|
+
...(raw.status ? { status: errorStatus(raw.status) } : {}) });
|
|
201
|
+
}
|
|
202
|
+
else if (raw.type === "result") {
|
|
203
|
+
totals({ ...record(raw.usage), total_cost_usd: raw.total_cost_usd }, "execution");
|
|
204
|
+
event("agent.result", { status: raw.is_error === true ? "error" : "ok", output: raw.result });
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const acp = (raw) => {
|
|
208
|
+
nativeSessionId = boundedId(raw.sessionId) ?? nativeSessionId;
|
|
209
|
+
const update = raw.update ? record(raw.update) : raw;
|
|
210
|
+
const kind = update.sessionUpdate;
|
|
211
|
+
if (kind === "agent_message_chunk" || kind === "agent_thought_chunk") {
|
|
212
|
+
const part = record(update.content);
|
|
213
|
+
if (part.type === "text")
|
|
214
|
+
buffer(part.text, kind === "agent_thought_chunk");
|
|
215
|
+
}
|
|
216
|
+
else if (kind === "tool_call" || kind === "tool_call_update") {
|
|
217
|
+
flush();
|
|
218
|
+
const complete = ["completed", "failed"].includes(String(update.status));
|
|
219
|
+
const name = string(update.kind) ? `tool.${update.kind}` : "tool.result";
|
|
220
|
+
if (kind === "tool_call" && !complete)
|
|
221
|
+
tool(update.toolCallId, "start", name, { input: update.rawInput });
|
|
222
|
+
if (complete)
|
|
223
|
+
tool(update.toolCallId, "end", name, {
|
|
224
|
+
input: update.rawInput, output: update.rawOutput ?? update.content, status: errorStatus(update.status),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else if (raw.type === "nowwork.acp.result") {
|
|
228
|
+
flush();
|
|
229
|
+
totals(raw.usage, "turn");
|
|
230
|
+
event("agent.result", { status: raw.stopReason === "end_turn" ? "ok" : "error", unavailable: ["per_generation_input", "per_generation_usage", "provider_request_id"] });
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const openCode = (raw) => {
|
|
234
|
+
nativeSessionId = boundedId(raw.sessionID) ?? nativeSessionId;
|
|
235
|
+
const part = record(raw.part);
|
|
236
|
+
if (raw.type === "tool_use") {
|
|
237
|
+
const state = record(part.state);
|
|
238
|
+
if (state.status === "running" || state.status === "pending")
|
|
239
|
+
tool(part.callID, "start", String(part.tool ?? "tool"), { input: state.input });
|
|
240
|
+
if (state.status === "completed" || state.status === "error")
|
|
241
|
+
tool(part.callID, "end", String(part.tool ?? "tool"), {
|
|
242
|
+
input: state.input, output: state.output ?? state.error, status: errorStatus(state.status),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
else if (raw.type === "step_start" || raw.type === "step_finish") {
|
|
246
|
+
const measured = usage({ ...record(part.tokens), costUsd: part.cost });
|
|
247
|
+
event(String(raw.type), { ...(measured ? { usage: measured, usageScope: "generation" } : {}),
|
|
248
|
+
unavailable: ["provider_request_id", "request_start_time", "per_generation_input"] });
|
|
249
|
+
}
|
|
250
|
+
else if (raw.type === "text" || raw.type === "reasoning") {
|
|
251
|
+
event(raw.type === "text" ? "model.output" : "model.reasoning", { output: part.text });
|
|
252
|
+
}
|
|
253
|
+
else if (raw.type === "error")
|
|
254
|
+
event("runtime.error", { status: "error", output: raw.error });
|
|
255
|
+
};
|
|
256
|
+
const pi = (raw) => {
|
|
257
|
+
if (raw.type === "session")
|
|
258
|
+
nativeSessionId = boundedId(raw.id) ?? nativeSessionId;
|
|
259
|
+
const assistant = record(raw.assistantMessageEvent);
|
|
260
|
+
if (raw.type === "message_update" && ["text_end", "thinking_end"].includes(String(assistant.type))) {
|
|
261
|
+
event(assistant.type === "text_end" ? "model.output" : "model.reasoning", { output: assistant.content });
|
|
262
|
+
}
|
|
263
|
+
else if (raw.type === "tool_execution_start") {
|
|
264
|
+
tool(raw.toolCallId, "start", String(raw.toolName ?? "tool"), { input: raw.args });
|
|
265
|
+
}
|
|
266
|
+
else if (raw.type === "tool_execution_end") {
|
|
267
|
+
tool(raw.toolCallId, "end", String(raw.toolName ?? "tool.result"), { output: raw.result, status: raw.isError === true ? "error" : "ok" });
|
|
268
|
+
}
|
|
269
|
+
else if (raw.type === "message_end" && record(raw.message).role === "assistant") {
|
|
270
|
+
const message = record(raw.message);
|
|
271
|
+
const measured = usage(message.usage);
|
|
272
|
+
event("model.response", { ...(measured ? { usage: measured, usageScope: "generation" } : {}),
|
|
273
|
+
...(string(message.model) ? { model: String(message.model) } : {}),
|
|
274
|
+
unavailable: ["provider_request_id", "request_start_time", "per_generation_input"] });
|
|
275
|
+
}
|
|
276
|
+
else if (raw.type === "error" || raw.type === "agent_error")
|
|
277
|
+
event("runtime.error", { status: "error", output: raw.error });
|
|
278
|
+
};
|
|
279
|
+
return {
|
|
280
|
+
enabled: capture.enabled,
|
|
281
|
+
context(input, unavailable = []) {
|
|
282
|
+
if (!capture.enabled)
|
|
283
|
+
return;
|
|
284
|
+
try {
|
|
285
|
+
event("context.input", { input, inputBytes: Buffer.byteLength(typeof input === "string" ? input : JSON.stringify(input)),
|
|
286
|
+
...(unavailable.length ? { unavailable: [...unavailable] } : {}) });
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
event("capture.unsupported", { unavailable: ["input_context"] });
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
observe(raw, method) {
|
|
293
|
+
if (!capture.enabled)
|
|
294
|
+
return;
|
|
295
|
+
try {
|
|
296
|
+
const value = record(raw);
|
|
297
|
+
if (runtime === "codex")
|
|
298
|
+
codex(method ?? "", value);
|
|
299
|
+
else if (runtime === "claude")
|
|
300
|
+
claude(value);
|
|
301
|
+
else if (runtime === "opencode")
|
|
302
|
+
openCode(value);
|
|
303
|
+
else if (runtime === "pi")
|
|
304
|
+
pi(value);
|
|
305
|
+
else
|
|
306
|
+
acp(value);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
event("capture.unsupported", { unavailable: ["invalid_runtime_event"] });
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
finish: flush,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
@@ -6,6 +6,8 @@ import spawn from "cross-spawn";
|
|
|
6
6
|
import { augmentedPath } from "../runtime-path.js";
|
|
7
7
|
import { prepareThreadDelivery } from "../thread-input-delivery.js";
|
|
8
8
|
import { buildOpenCodeArgs, createOpenCodeEventDecoder } from "./opencode.js";
|
|
9
|
+
import { createRuntimeObservationCapture } from "../runtime-observation.js";
|
|
10
|
+
import { createObservationMapper } from "./observation-mapper.js";
|
|
9
11
|
const ERROR_CAP = 2_000;
|
|
10
12
|
async function readPrompt() {
|
|
11
13
|
process.stdin.setEncoding("utf8");
|
|
@@ -25,7 +27,8 @@ export function isOpenCodeSessionNotFound(message) {
|
|
|
25
27
|
return /(?:session|conversation).*(?:not found|does not exist|unknown|invalid)|(?:not found|unknown|invalid).*(?:session|conversation)/iu
|
|
26
28
|
.test(message);
|
|
27
29
|
}
|
|
28
|
-
async function runAttempt(options, prompt) {
|
|
30
|
+
async function runAttempt(options, prompt, capture, attempt) {
|
|
31
|
+
const observer = createObservationMapper("opencode", capture, `attempt:${attempt}`);
|
|
29
32
|
const cwd = process.cwd();
|
|
30
33
|
const child = spawn(options.bin, buildOpenCodeArgs({ cwd, ...options }), {
|
|
31
34
|
cwd,
|
|
@@ -37,6 +40,7 @@ async function runAttempt(options, prompt) {
|
|
|
37
40
|
}
|
|
38
41
|
child.stderr.pipe(process.stderr, { end: false });
|
|
39
42
|
const childExit = once(child, "close");
|
|
43
|
+
observer.context({ prompt });
|
|
40
44
|
child.stdin.end(prompt);
|
|
41
45
|
const decoder = createOpenCodeEventDecoder();
|
|
42
46
|
const lines = createInterface({ input: child.stdout });
|
|
@@ -50,6 +54,7 @@ async function runAttempt(options, prompt) {
|
|
|
50
54
|
catch {
|
|
51
55
|
continue;
|
|
52
56
|
}
|
|
57
|
+
observer.observe(event);
|
|
53
58
|
for (const normalized of decoder.push(event)) {
|
|
54
59
|
const type = normalized.type;
|
|
55
60
|
const semantic = type === "opencode.text_delta"
|
|
@@ -87,17 +92,26 @@ async function runAttempt(options, prompt) {
|
|
|
87
92
|
return { code: 0, staleSession: false };
|
|
88
93
|
}
|
|
89
94
|
export async function runOpenCode(options) {
|
|
95
|
+
const capture = createRuntimeObservationCapture("opencode");
|
|
96
|
+
try {
|
|
97
|
+
return await runOpenCodeObserved(options, capture);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
capture.finish();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function runOpenCodeObserved(options, capture) {
|
|
90
104
|
const prompt = (await prepareThreadDelivery({ env: process.env, resumed: false,
|
|
91
105
|
sessionId: "unverified-cold-opencode", wakePrompt: await readPrompt() })).prompt;
|
|
92
106
|
if (process.env.CREW_THREAD_ROOT) {
|
|
93
107
|
const { sessionId: _unverified, ...coldOptions } = options;
|
|
94
|
-
return (await runAttempt(coldOptions, prompt)).code;
|
|
108
|
+
return (await runAttempt(coldOptions, prompt, capture, 1)).code;
|
|
95
109
|
}
|
|
96
|
-
const first = await runAttempt(options, prompt);
|
|
110
|
+
const first = await runAttempt(options, prompt, capture, 1);
|
|
97
111
|
if (!first.staleSession)
|
|
98
112
|
return first.code;
|
|
99
113
|
const { sessionId: _staleSessionId, ...freshOptions } = options;
|
|
100
|
-
return (await runAttempt(freshOptions, prompt)).code;
|
|
114
|
+
return (await runAttempt(freshOptions, prompt, capture, 2)).code;
|
|
101
115
|
}
|
|
102
116
|
function optionsFromArgv(argv) {
|
|
103
117
|
const { values } = parseArgs({
|
|
@@ -6,11 +6,24 @@ import spawn from "cross-spawn";
|
|
|
6
6
|
import { augmentedPath } from "../runtime-path.js";
|
|
7
7
|
import { prepareThreadDelivery } from "../thread-input-delivery.js";
|
|
8
8
|
import { buildPiArgs, createPiEventDecoder } from "./pi.js";
|
|
9
|
+
import { createRuntimeObservationCapture } from "../runtime-observation.js";
|
|
10
|
+
import { createObservationMapper } from "./observation-mapper.js";
|
|
9
11
|
async function readInput() { process.stdin.setEncoding("utf8"); let text = ""; for await (const chunk of process.stdin)
|
|
10
12
|
text += String(chunk); return JSON.parse(text); }
|
|
11
13
|
async function emit(event) { if (process.stdout.write(`${JSON.stringify(event)}\n`))
|
|
12
14
|
return; await once(process.stdout, "drain"); }
|
|
13
15
|
export async function runPi(bin) {
|
|
16
|
+
const capture = createRuntimeObservationCapture("pi");
|
|
17
|
+
const observer = createObservationMapper("pi", capture);
|
|
18
|
+
try {
|
|
19
|
+
return await runPiObserved(bin, observer);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
observer.finish();
|
|
23
|
+
capture.finish();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function runPiObserved(bin, observer) {
|
|
14
27
|
const input = await readInput();
|
|
15
28
|
// Pi 不持久化原生会话,每轮都须在启动模型前补齐当前线程快照。
|
|
16
29
|
const delivery = await prepareThreadDelivery({
|
|
@@ -27,11 +40,14 @@ export async function runPi(bin) {
|
|
|
27
40
|
let inputError;
|
|
28
41
|
child.stdin.on("error", error => { inputError = error; });
|
|
29
42
|
// Pi print 模式读取 stdin;完整线程历史不能塞进受长度限制的 argv。
|
|
43
|
+
observer.context({ systemPrompt: input.systemPrompt, prompt: delivery.prompt });
|
|
30
44
|
child.stdin.end(delivery.prompt);
|
|
31
45
|
const decoder = createPiEventDecoder();
|
|
32
46
|
for await (const line of createInterface({ input: child.stdout })) {
|
|
33
47
|
try {
|
|
34
|
-
|
|
48
|
+
const raw = JSON.parse(line);
|
|
49
|
+
observer.observe(raw);
|
|
50
|
+
for (const event of decoder.push(raw))
|
|
35
51
|
await emit(event);
|
|
36
52
|
}
|
|
37
53
|
catch { }
|
package/dist/token.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseObservabilityPolicy } from "./runtime-observation.js";
|
|
1
2
|
export async function mintAgentToken(serverUrl, machineToken, handle, displayName, options = {}, fetchImpl = fetch) {
|
|
2
3
|
const res = await fetchImpl(`${serverUrl}/daemon/agents/token`, {
|
|
3
4
|
method: "POST",
|
|
@@ -19,5 +20,7 @@ export async function mintAgentToken(serverUrl, machineToken, handle, displayNam
|
|
|
19
20
|
if (!res.ok || !body?.success || !body.data) {
|
|
20
21
|
throw new Error(`换取 agent 令牌失败 (HTTP ${res.status}): ${body?.error?.message ?? ""}`);
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
+
const { observability: rawPolicy, ...credential } = body.data;
|
|
24
|
+
const observability = options.executionId ? parseObservabilityPolicy(rawPolicy) : undefined;
|
|
25
|
+
return { ...credential, ...(observability ? { observability } : {}) };
|
|
23
26
|
}
|