@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.
- package/README.md +29 -20
- package/dist/automatic-daemon-installer.js +15 -11
- package/dist/automatic-service-capture.js +27 -0
- package/dist/computer-main.js +0 -0
- package/dist/computer-service.js +30 -5
- package/dist/console.js +17 -2
- package/dist/execution-journal-lock.js +51 -2
- package/dist/execution-protocol.js +2 -0
- package/dist/execution-runner.js +19 -13
- package/dist/host-execution-coordinator.js +10 -0
- package/dist/local-executor.js +8 -1
- package/dist/machine-info.js +6 -1
- package/dist/machine-runtime-environment.js +1 -0
- package/dist/main.js +0 -0
- package/dist/managed-service-lifecycle.js +10 -0
- package/dist/marketplace-controller.js +21 -8
- package/dist/marketplace-maintenance-bridge.js +79 -0
- package/dist/marketplace-maintenance-protocol.js +40 -0
- package/dist/marketplace-serve.js +4 -2
- package/dist/project-skills/execution-projection.js +4 -5
- package/dist/project-skills/reconciler.js +8 -9
- package/dist/project-skills/scanner.js +9 -2
- package/dist/project-skills/source-directory.js +13 -0
- package/dist/runtime-observation.js +219 -0
- package/dist/runtime-tool-update-bridge.js +126 -0
- package/dist/runtime-tool-update-controller.js +151 -0
- package/dist/runtime-tool-update-protocol.js +24 -0
- package/dist/runtime-tool-update-serve.js +54 -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 +18 -1
- package/dist/runtimes/pi.js +54 -20
- package/dist/serve.js +10 -1
- package/dist/token.js +4 -1
- 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
|
-
|
|
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;
|
package/dist/runtimes/pi.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
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
|
-
//
|
|
91
|
+
// turn_end 只是 Pi 内部一轮:工具调用后会继续,provider 出错还会自动重试(agent_end.willRetry)。
|
|
52
92
|
if (e.message?.role === "assistant") {
|
|
53
93
|
lastAssistant = e.message;
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
78
|
-
const failure =
|
|
79
|
-
|
|
80
|
-
:
|
|
81
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|