@nowcrew/daemon 0.6.98-beta.6 → 0.6.98-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +29 -20
  2. package/dist/automatic-daemon-installer.js +15 -11
  3. package/dist/automatic-service-capture.js +27 -0
  4. package/dist/computer-main.js +0 -0
  5. package/dist/computer-service.js +30 -5
  6. package/dist/console.js +17 -2
  7. package/dist/execution-journal-lock.js +51 -2
  8. package/dist/execution-protocol.js +2 -0
  9. package/dist/execution-runner.js +19 -13
  10. package/dist/host-execution-coordinator.js +10 -0
  11. package/dist/local-executor.js +8 -1
  12. package/dist/machine-info.js +6 -1
  13. package/dist/machine-runtime-environment.js +1 -0
  14. package/dist/main.js +0 -0
  15. package/dist/managed-service-lifecycle.js +10 -0
  16. package/dist/marketplace-controller.js +21 -8
  17. package/dist/marketplace-maintenance-bridge.js +79 -0
  18. package/dist/marketplace-maintenance-protocol.js +40 -0
  19. package/dist/marketplace-serve.js +4 -2
  20. package/dist/project-skills/execution-projection.js +4 -5
  21. package/dist/project-skills/reconciler.js +8 -9
  22. package/dist/project-skills/scanner.js +9 -2
  23. package/dist/project-skills/source-directory.js +13 -0
  24. package/dist/runtime-observation.js +219 -0
  25. package/dist/runtime-tool-update-bridge.js +126 -0
  26. package/dist/runtime-tool-update-controller.js +151 -0
  27. package/dist/runtime-tool-update-protocol.js +24 -0
  28. package/dist/runtime-tool-update-serve.js +54 -0
  29. package/dist/runtimes/claude-observation-runner.js +55 -0
  30. package/dist/runtimes/claude-thread-runner.js +17 -3
  31. package/dist/runtimes/claude.js +5 -3
  32. package/dist/runtimes/codex-app-server-runner.js +16 -2
  33. package/dist/runtimes/deepseek-harness-runner.js +16 -0
  34. package/dist/runtimes/kimi-acp-runner.js +19 -0
  35. package/dist/runtimes/observation-context.js +18 -0
  36. package/dist/runtimes/observation-mapper.js +314 -0
  37. package/dist/runtimes/opencode-runner.js +18 -4
  38. package/dist/runtimes/pi-runner.js +18 -1
  39. package/dist/runtimes/pi.js +54 -20
  40. package/dist/serve.js +10 -1
  41. package/dist/token.js +4 -1
  42. package/package.json +9 -9
@@ -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
- for (const event of decoder.push(JSON.parse(line)))
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 { }
@@ -40,6 +56,7 @@ export async function runPi(bin) {
40
56
  if (inputError)
41
57
  throw new Error(`Pi prompt delivery failed: ${inputError.message}`);
42
58
  const result = decoder.finish();
59
+ await emit(result.event);
43
60
  if (!result.ok) {
44
61
  process.stderr.write(`${result.error}\n`);
45
62
  return 1;
@@ -3,6 +3,23 @@ export function buildPiArgs(options) {
3
3
  ...(options.model ? ["--model", options.model] : []),
4
4
  ...(options.reasoning && options.reasoning !== "default" ? ["--thinking", options.reasoning] : [])];
5
5
  }
6
+ const TOOL_ARG_KEYS = ["command", "path", "file_path", "pattern", "url"];
7
+ function toolTitle(name, args) {
8
+ const arg = TOOL_ARG_KEYS.map(key => args?.[key]).find((value) => typeof value === "string" && !!value.trim());
9
+ return arg ? `${name}(${arg.trim()})` : name;
10
+ }
11
+ function toolResultText(result) {
12
+ return (result?.content ?? []).map(item => item.type === "text" && typeof item.text === "string" ? item.text : "")
13
+ .filter(Boolean).join("\n");
14
+ }
15
+ function addUsage(total, next) {
16
+ if (!next)
17
+ return total;
18
+ return {
19
+ input: (total?.input ?? 0) + (next.input ?? 0), output: (total?.output ?? 0) + (next.output ?? 0),
20
+ cacheRead: (total?.cacheRead ?? 0) + (next.cacheRead ?? 0), cacheWrite: (total?.cacheWrite ?? 0) + (next.cacheWrite ?? 0),
21
+ };
22
+ }
6
23
  // 保留稳定的原因标识供 server 本地化;不要用 token 数量猜测结束原因。
7
24
  function failureReason(message) {
8
25
  const reason = message?.stopReason;
@@ -15,7 +32,17 @@ export function createPiEventDecoder() {
15
32
  let lastAssistant = null;
16
33
  let assistantOpen = false;
17
34
  let sawTurnEnd = false;
35
+ let turnUsageCounted = false;
18
36
  let usage;
37
+ // 正常 stop 可为空(任务可能要求静默);length/error/aborted 或停在工具轮都必须明确失败。
38
+ const runFailure = () => error ?? (assistantOpen
39
+ ? "pi_runtime_failed: incomplete"
40
+ : failureReason(lastAssistant)
41
+ ?? (!sawTurnEnd || lastAssistant?.stopReason === "toolUse" ? "pi_runtime_failed: incomplete" : null));
42
+ const normalizedUsage = () => usage ? { usage: {
43
+ input_tokens: usage.input ?? 0, output_tokens: usage.output ?? 0,
44
+ cache_read_input_tokens: usage.cacheRead ?? 0, cache_creation_input_tokens: usage.cacheWrite ?? 0,
45
+ } } : {};
19
46
  return {
20
47
  push(raw) {
21
48
  const e = (raw ?? {});
@@ -24,8 +51,10 @@ export function createPiEventDecoder() {
24
51
  sessionId = e.id;
25
52
  out.push({ type: "thread.started", thread_id: e.id });
26
53
  }
27
- if (e.type === "turn_start")
54
+ if (e.type === "turn_start") {
28
55
  sawTurnEnd = false;
56
+ turnUsageCounted = false;
57
+ }
29
58
  if ((e.type === "message_start" || e.type === "message_update") && e.message?.role === "assistant") {
30
59
  assistantOpen = true;
31
60
  sawTurnEnd = false;
@@ -38,34 +67,39 @@ export function createPiEventDecoder() {
38
67
  if (e.type === "message_update" && ae?.type === "thinking_delta" && typeof ae.delta === "string") {
39
68
  out.push({ type: "pi.thinking_delta", text: ae.delta });
40
69
  }
41
- if (e.type === "message_update" && typeof ae?.type === "string" && /tool(call|_call)/iu.test(ae.type)) {
42
- out.push({ type: "pi.tool_call", ...(ae.name ? { title: ae.name } : {}) });
70
+ // delta 逐 token 到达,console 只消费块结束事件,避免一个 token 一行。
71
+ if (e.type === "message_update" && ae?.type === "text_end" && ae.content?.trim()) {
72
+ out.push({ type: "pi.text", text: ae.content.trim() });
73
+ }
74
+ if (e.type === "message_update" && ae?.type === "thinking_end" && ae.content?.trim()) {
75
+ out.push({ type: "pi.thinking", text: ae.content.trim() });
76
+ }
77
+ if (e.type === "tool_execution_start" && typeof e.toolName === "string") {
78
+ out.push({ type: "pi.tool_call", title: toolTitle(e.toolName, e.args) });
79
+ }
80
+ if (e.type === "tool_execution_end") {
81
+ out.push({ type: "pi.tool_result", content: toolResultText(e.result), is_error: e.isError === true });
43
82
  }
44
83
  if (e.type === "message_end" && e.message?.role === "assistant") {
45
84
  lastAssistant = e.message;
46
- usage = e.message.usage;
85
+ usage = addUsage(usage, e.message.usage);
86
+ turnUsageCounted = true;
47
87
  assistantOpen = false;
48
88
  sawTurnEnd = false;
49
89
  }
50
90
  if (e.type === "turn_end") {
51
- // 原生 turn_end 自带本轮消息,不能把工具调用轮的结束当成整次执行成功。
91
+ // turn_end 只是 Pi 内部一轮:工具调用后会继续,provider 出错还会自动重试(agent_end.willRetry)。
52
92
  if (e.message?.role === "assistant") {
53
93
  lastAssistant = e.message;
54
- usage = e.message.usage;
94
+ if (!turnUsageCounted)
95
+ usage = addUsage(usage, e.message.usage);
55
96
  assistantOpen = false;
56
97
  }
57
98
  else if (e.message) {
58
99
  lastAssistant = null;
59
100
  }
60
101
  sawTurnEnd = true;
61
- const normalizedUsage = usage ? { usage: {
62
- input_tokens: usage.input ?? 0, output_tokens: usage.output ?? 0,
63
- cache_read_input_tokens: usage.cacheRead ?? 0, cache_creation_input_tokens: usage.cacheWrite ?? 0,
64
- } } : {};
65
- const failure = failureReason(lastAssistant);
66
- out.push(failure
67
- ? { type: "result", is_error: true, result: failure, ...normalizedUsage }
68
- : { type: "turn.completed", ...normalizedUsage });
102
+ turnUsageCounted = false;
69
103
  }
70
104
  if (e.type === "error" || e.type === "agent_error") {
71
105
  error = typeof e.error === "string" ? e.error : (e.error?.message ?? "Pi error");
@@ -74,12 +108,12 @@ export function createPiEventDecoder() {
74
108
  return out;
75
109
  },
76
110
  finish() {
77
- // 正常 stop 可为空(任务可能要求静默);length/error/aborted 必须明确失败。
78
- const failure = error ?? (assistantOpen
79
- ? "pi_runtime_failed: incomplete"
80
- : failureReason(lastAssistant)
81
- ?? (!sawTurnEnd || lastAssistant?.stopReason === "toolUse" ? "pi_runtime_failed: incomplete" : null));
82
- return { ok: failure === null, ...(failure ? { error: failure } : {}), finalText, sessionId, usage };
111
+ // 终态只在 stdout 结束后产出一次:重试/中断都以最终状态为准,usage 覆盖所有已完成轮次。
112
+ const failure = runFailure();
113
+ const event = failure
114
+ ? { type: "result", is_error: true, result: failure, ...normalizedUsage() }
115
+ : { type: "turn.completed", ...normalizedUsage() };
116
+ return { ok: failure === null, ...(failure ? { error: failure } : {}), finalText, sessionId, usage, event };
83
117
  },
84
118
  };
85
119
  }
package/dist/serve.js CHANGED
@@ -48,6 +48,7 @@ import { initializedProjectSkillsReconciler } from "./project-skills/initialized
48
48
  import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
49
49
  import { probeRuntimeHealth } from "./runtime-health.js";
50
50
  import { createServeMarketplace } from "./marketplace-serve.js";
51
+ import { createServeRuntimeToolUpdates } from "./runtime-tool-update-serve.js";
51
52
  import { buildControlPlaneUrl } from "./control-plane-url.js";
52
53
  import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
53
54
  import { bindStorageServerIdentity, createStorageIdentityAdmission, } from "./storage-serve-adapter.js";
@@ -122,7 +123,7 @@ export function serve(config, opts = {}) {
122
123
  catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
123
124
  },
124
125
  });
125
- const marketplace = createServeMarketplace(config, localSlots, hostCoordinator);
126
+ const marketplace = createServeMarketplace(config, localSlots, hostCoordinator, async () => ({ ...(await machineRuntime.provider.acquire()).env }));
126
127
  const storageIdentityAdmission = createStorageIdentityAdmission(opts.storageRuntime);
127
128
  const projectionCoordinator = createAgentProjectionCoordinator();
128
129
  const { projectRegistry, projectWorkspaceRegistry, projectDependencyProvisioner } = createProjectRegistryWiring(config.agentsRoot, opts.execution?.dependencies);
@@ -210,6 +211,7 @@ export function serve(config, opts = {}) {
210
211
  onError: (code, context) => dslog("execution.runtime_detection_failed", "机器环境或 runtime 探测失败", { level: "ERROR", error_code: code, ...context }),
211
212
  });
212
213
  machineRuntime.prewarm();
214
+ const runtimeToolUpdates = createServeRuntimeToolUpdates(config, localSlots, hostCoordinator, async () => ({ ...(await machineRuntime.provider.acquire()).env }), () => machineRuntime.refresh());
213
215
  const initializedProjectSkills = initializedProjectSkillsReconciler(() => projectSkillsController.ensureScanned(), projectSkillsReconciler);
214
216
  const configuredCapabilities = opts.machineInfo?.capabilities ?? daemonCapabilities(process.platform);
215
217
  const prepareCapabilities = async () => daemonCapabilityBindings(process.platform, [...configuredCapabilities, ...await managedDaemonCapabilities(updateEligibility)]);
@@ -842,6 +844,12 @@ export function serve(config, opts = {}) {
842
844
  }
843
845
  return;
844
846
  }
847
+ const runtimeUpdateSocket = ws;
848
+ if (runtimeToolUpdates.handle(msg, frame => { try {
849
+ runtimeUpdateSocket?.send(JSON.stringify(frame));
850
+ }
851
+ catch { /* Server reconciles via status. */ } }))
852
+ return;
845
853
  if (marketplace.handle(msg, frame => { try {
846
854
  ws?.send(JSON.stringify(frame));
847
855
  }
@@ -1326,6 +1334,7 @@ export function serve(config, opts = {}) {
1326
1334
  webSocketClosed,
1327
1335
  updateController.drain(),
1328
1336
  marketplace.stop(),
1337
+ runtimeToolUpdates.stop(),
1329
1338
  executionFrameQueue,
1330
1339
  ...executionRuns.values(),
1331
1340
  ...[...legacyRuns.values()].map((run) => run.done),
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
- return body.data;
23
+ const { observability: rawPolicy, ...credential } = body.data;
24
+ const observability = options.executionId ? parseObservabilityPolicy(rawPolicy) : undefined;
25
+ return { ...credential, ...(observability ? { observability } : {}) };
23
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.98-beta.6",
3
+ "version": "0.6.98-beta.8",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -18,13 +18,6 @@
18
18
  "publishConfig": {
19
19
  "access": "public"
20
20
  },
21
- "scripts": {
22
- "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
23
- "build": "tsc -p tsconfig.json",
24
- "prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
25
- "test": "vitest run",
26
- "typecheck": "tsc --noEmit"
27
- },
28
21
  "dependencies": {
29
22
  "@agentclientprotocol/sdk": "1.2.1",
30
23
  "@nowcrew/cli": "0.4.17",
@@ -46,5 +39,12 @@
46
39
  "tsx": "^4.19.0",
47
40
  "typescript": "^5.6.0",
48
41
  "vitest": "^2.1.0"
42
+ },
43
+ "scripts": {
44
+ "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
45
+ "build": "tsc -p tsconfig.json",
46
+ "test": "vitest run",
47
+ "test:release-critical": "vitest run test/automatic-daemon-installer.test.ts test/automatic-install-target.test.ts test/automatic-service-migration.test.ts test/computer-cli.test.ts test/computer-service.test.ts test/computer-setup-client.test.ts test/computer-setup-orchestration.test.ts test/computer-setup.test.ts test/daemon-release-version.test.ts test/daemon-update-controller.test.ts test/daemon-update-eligibility.test.ts test/daemon-updater.test.ts test/execution-protocol.test.ts test/machine-info.test.ts --maxWorkers=2 --minWorkers=1",
48
+ "typecheck": "tsc --noEmit"
49
49
  }
50
- }
50
+ }