@nowcrew/daemon 0.5.45 → 0.5.46
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 +22 -3
- package/dist/agent-memory/bridge.js +101 -10
- package/dist/execution-journal.js +1 -1
- package/dist/execution-protocol.js +3 -11
- package/dist/execution-runner.js +13 -4
- package/dist/external-output.js +4 -0
- package/dist/list-models.js +102 -8
- package/dist/local-executor.js +87 -14
- package/dist/local-memory-diagnostics.js +336 -0
- package/dist/local-memory-telemetry.js +224 -0
- package/dist/machine-info.js +22 -7
- package/dist/main.js +6 -1
- package/dist/normalize.js +14 -4
- package/dist/runtime-capabilities.js +11 -1
- package/dist/runtime-probe.js +26 -0
- package/dist/runtime-startup-gate.js +2 -0
- package/dist/runtimes/codex-app-server-runner.js +10 -0
- package/dist/runtimes/hermes-models.js +117 -0
- package/dist/runtimes/hermes.js +6 -0
- package/dist/runtimes/kimi-acp-runner.js +116 -36
- package/dist/runtimes/opencode-runner.js +122 -0
- package/dist/runtimes/opencode.js +181 -0
- package/dist/serve.js +27 -4
- package/dist/slog.js +21 -3
- package/dist/supervised-runtime.js +22 -1
- package/dist/workspace.js +34 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,8 +193,8 @@ dispatch reports unavailable instead of running on another compatible daemon. An
|
|
|
193
193
|
the global compatible-machine pool.
|
|
194
194
|
|
|
195
195
|
The daemon intersects requested permission with local policy, checks advertised resource limits, prepares
|
|
196
|
-
the local workspace/environment, and launches only a built-in runtime adapter (`claude`, `codex`,
|
|
197
|
-
`
|
|
196
|
+
the local workspace/environment, and launches only a built-in runtime adapter (`claude`, `codex`, `kimi`,
|
|
197
|
+
`hermes`, or `opencode`). It does not decide collaboration rules, scheduled output policy, fallback delivery, or thread
|
|
198
198
|
behavior. Those are server responsibilities.
|
|
199
199
|
|
|
200
200
|
The stable adapters keep prompts out of provider argv wherever the provider protocol allows it:
|
|
@@ -204,6 +204,8 @@ The stable adapters keep prompts out of provider argv wherever the provider prot
|
|
|
204
204
|
| Claude | stream-json CLI | daemon-owned system prompt file | CLI session id |
|
|
205
205
|
| Codex | app-server JSON-RPC over stdio | `developerInstructions` + turn input | `thread/resume` |
|
|
206
206
|
| Kimi | ACP over stdio | `session/prompt` | `session/resume` (`session/load` compatibility fallback) |
|
|
207
|
+
| Hermes | ACP over stdio | `session/prompt` | `session/resume` (`session/load` compatibility fallback) |
|
|
208
|
+
| OpenCode | `run --format json` | stdin | `--session` |
|
|
207
209
|
|
|
208
210
|
Kimi ACP currently requires a Kimi account even when the CLI has a working custom provider. Only for
|
|
209
211
|
the explicit `Authentication required` response, the adapter retries once through Kimi's stream-json
|
|
@@ -211,7 +213,20 @@ CLI and carries the same native session id; unrelated ACP failures stay failures
|
|
|
211
213
|
subject to the Windows UTF-16 argv guard, while protocol-v1 itself remains disabled on Windows until
|
|
212
214
|
durable Job Object ownership is available.
|
|
213
215
|
|
|
214
|
-
|
|
216
|
+
Hermes uses only ACP capabilities advertised by the installed CLI. Model and effort choices come from
|
|
217
|
+
a short-lived discovery session; credentials, provider profiles, plugins, and `HERMES_HOME` remain
|
|
218
|
+
machine-local. A missing resumed ACP session is retried once as a fresh session.
|
|
219
|
+
|
|
220
|
+
OpenCode receives the prompt through stdin and runs with both cwd and `PWD` anchored to the Agent
|
|
221
|
+
workspace. Models and variants come from `opencode models --verbose` (with the plain catalog as a
|
|
222
|
+
compatibility fallback). A missing resumed session is retried once without `--session`; malformed,
|
|
223
|
+
empty, incomplete, or explicitly failed JSON streams remain failures. NowWork never writes
|
|
224
|
+
`opencode.json` or replaces `OPENCODE_CONFIG_CONTENT`. The daemon advertises OpenCode as executable
|
|
225
|
+
only when `opencode run --help` exposes `--format`, `--dangerously-skip-permissions`, `--dir`,
|
|
226
|
+
`--model`, `--variant`, and `--session`; older installations remain visible as detected CLIs until
|
|
227
|
+
they are upgraded, rather than falling back to a permission override that user config could weaken.
|
|
228
|
+
|
|
229
|
+
Codex, Kimi, Hermes, and OpenCode sessions use the same bounded per-task persistence and keyed lease as Claude when they
|
|
215
230
|
run through protocol v1. A first-progress watchdog covers a provider that accepts a turn but remains
|
|
216
231
|
semantically silent; after the first semantic event, the execution's configured total timeout remains
|
|
217
232
|
authoritative so a legitimate long-running tool is not killed for quiet output.
|
|
@@ -219,6 +234,10 @@ authoritative so a legitimate long-running tool is not killed for quiet output.
|
|
|
219
234
|
Protocol support and limits are advertised in `machine:hello`. `runtimes` reports every recognized CLI
|
|
220
235
|
found on `PATH`; `executionRuntimes` separately reports the installed CLIs backed by a complete built-in
|
|
221
236
|
adapter. The server must use the latter for admission and treats the former as diagnostic inventory only.
|
|
237
|
+
The daemon sends a conservative first hello without waiting for third-party handshakes, then refreshes it
|
|
238
|
+
after the optional Kimi/Hermes/OpenCode probes finish. Work targeting those optional runtimes waits for the
|
|
239
|
+
same full probe result; reconnects share one in-flight probe, and shutdown aborts any unfinished probe and
|
|
240
|
+
its child process.
|
|
222
241
|
Old daemons that omit `executionRuntimes` are conservatively interpreted as the intersection of installed
|
|
223
242
|
CLIs and server-supported adapters. Unknown required protocol semantics are rejected before side effects.
|
|
224
243
|
Protocol-0 `agent:start` remains only for the server-governed compatibility window.
|
|
@@ -1,35 +1,126 @@
|
|
|
1
1
|
import { createAgentMemoryClient } from "./client.js";
|
|
2
2
|
import { buildCaptureMessages, extractIncomingMessage, rankMemoryItems, renderMemoryContext, } from "./policy.js";
|
|
3
|
+
import { dslog } from "../slog.js";
|
|
4
|
+
function safeReport(report, eventType, message, fields) {
|
|
5
|
+
try {
|
|
6
|
+
report(eventType, message, fields);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
// Diagnostics must never affect optional memory recall or capture.
|
|
10
|
+
}
|
|
11
|
+
}
|
|
3
12
|
export function createAgentMemoryBridge(config, dependencies = {}) {
|
|
4
13
|
const client = dependencies.client ?? createAgentMemoryClient(config);
|
|
14
|
+
const report = dependencies.report ?? dslog;
|
|
5
15
|
const blockId = `chat_memory-${config.teamId}-${config.agentId}`;
|
|
6
16
|
return {
|
|
7
|
-
recall: async (agentHandle, wakePrompt) => {
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
17
|
+
recall: async (agentHandle, wakePrompt, executionId) => {
|
|
18
|
+
const startedAt = Date.now();
|
|
19
|
+
if (agentHandle !== config.agentHandle) {
|
|
20
|
+
safeReport(report, "external_agent_memory.recall_observed", "外部 Agent Memory recall 已跳过", {
|
|
21
|
+
execution_id: executionId,
|
|
22
|
+
agent_handle: agentHandle,
|
|
23
|
+
outcome: "skipped",
|
|
24
|
+
skip_reason: "handle_mismatch",
|
|
25
|
+
duration_ms: Date.now() - startedAt,
|
|
26
|
+
});
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
const incoming = extractIncomingMessage(wakePrompt);
|
|
30
|
+
if (incoming === null) {
|
|
31
|
+
safeReport(report, "external_agent_memory.recall_observed", "外部 Agent Memory recall 已跳过", {
|
|
32
|
+
execution_id: executionId,
|
|
33
|
+
agent_handle: agentHandle,
|
|
34
|
+
outcome: "skipped",
|
|
35
|
+
skip_reason: "wake_unparsed",
|
|
36
|
+
duration_ms: Date.now() - startedAt,
|
|
37
|
+
});
|
|
10
38
|
return "";
|
|
39
|
+
}
|
|
11
40
|
try {
|
|
12
41
|
const [core, atomic] = await Promise.all([
|
|
13
42
|
client.layer(blockId, "L3", 1),
|
|
14
43
|
client.layer(blockId, "L1", config.recallLimit),
|
|
15
44
|
]);
|
|
16
|
-
|
|
45
|
+
const selected = rankMemoryItems(incoming, atomic, config.recallLimit);
|
|
46
|
+
const rendered = renderMemoryContext(core, selected);
|
|
47
|
+
safeReport(report, "external_agent_memory.recall_observed", "外部 Agent Memory recall 已完成", {
|
|
48
|
+
execution_id: executionId,
|
|
49
|
+
agent_handle: agentHandle,
|
|
50
|
+
outcome: "succeeded",
|
|
51
|
+
core_count: core.length,
|
|
52
|
+
atomic_count: atomic.length,
|
|
53
|
+
selected_atomic_count: selected.length,
|
|
54
|
+
rendered_bytes: Buffer.byteLength(rendered, "utf8"),
|
|
55
|
+
duration_ms: Date.now() - startedAt,
|
|
56
|
+
});
|
|
57
|
+
return rendered;
|
|
17
58
|
}
|
|
18
|
-
catch {
|
|
59
|
+
catch (error) {
|
|
60
|
+
safeReport(report, "external_agent_memory.recall_observed", "外部 Agent Memory recall 失败并已放行", {
|
|
61
|
+
level: "WARN",
|
|
62
|
+
execution_id: executionId,
|
|
63
|
+
agent_handle: agentHandle,
|
|
64
|
+
outcome: "failed",
|
|
65
|
+
error_name: error instanceof Error ? error.name : "unknown",
|
|
66
|
+
duration_ms: Date.now() - startedAt,
|
|
67
|
+
});
|
|
19
68
|
return "";
|
|
20
69
|
}
|
|
21
70
|
},
|
|
22
71
|
capture: async (agentHandle, executionId, wakePrompt, finalText) => {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
72
|
+
const startedAt = Date.now();
|
|
73
|
+
if (agentHandle !== config.agentHandle) {
|
|
74
|
+
safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 已跳过", {
|
|
75
|
+
execution_id: executionId,
|
|
76
|
+
agent_handle: agentHandle,
|
|
77
|
+
outcome: "skipped",
|
|
78
|
+
skip_reason: "handle_mismatch",
|
|
79
|
+
duration_ms: Date.now() - startedAt,
|
|
80
|
+
});
|
|
25
81
|
return;
|
|
82
|
+
}
|
|
83
|
+
const incoming = extractIncomingMessage(wakePrompt);
|
|
84
|
+
if (incoming === null) {
|
|
85
|
+
safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 已跳过", {
|
|
86
|
+
execution_id: executionId,
|
|
87
|
+
agent_handle: agentHandle,
|
|
88
|
+
outcome: "skipped",
|
|
89
|
+
skip_reason: "wake_unparsed",
|
|
90
|
+
duration_ms: Date.now() - startedAt,
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
26
94
|
const messages = buildCaptureMessages(incoming, finalText);
|
|
27
|
-
if (messages === null)
|
|
95
|
+
if (messages === null) {
|
|
96
|
+
safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 已跳过", {
|
|
97
|
+
execution_id: executionId,
|
|
98
|
+
agent_handle: agentHandle,
|
|
99
|
+
outcome: "skipped",
|
|
100
|
+
skip_reason: "content_filtered",
|
|
101
|
+
duration_ms: Date.now() - startedAt,
|
|
102
|
+
});
|
|
28
103
|
return;
|
|
104
|
+
}
|
|
29
105
|
try {
|
|
30
|
-
await client.importConversation(`nowwork-${executionId}`, messages);
|
|
106
|
+
const result = await client.importConversation(`nowwork-${executionId}`, messages);
|
|
107
|
+
safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 已完成", {
|
|
108
|
+
execution_id: executionId,
|
|
109
|
+
agent_handle: agentHandle,
|
|
110
|
+
outcome: "succeeded",
|
|
111
|
+
accepted_count: result.acceptedCount,
|
|
112
|
+
duration_ms: Date.now() - startedAt,
|
|
113
|
+
});
|
|
31
114
|
}
|
|
32
|
-
catch {
|
|
115
|
+
catch (error) {
|
|
116
|
+
safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 失败并已放行", {
|
|
117
|
+
level: "WARN",
|
|
118
|
+
execution_id: executionId,
|
|
119
|
+
agent_handle: agentHandle,
|
|
120
|
+
outcome: "failed",
|
|
121
|
+
error_name: error instanceof Error ? error.name : "unknown",
|
|
122
|
+
duration_ms: Date.now() - startedAt,
|
|
123
|
+
});
|
|
33
124
|
// External memory is an optional side effect and cannot alter execution completion.
|
|
34
125
|
}
|
|
35
126
|
},
|
|
@@ -7,7 +7,7 @@ import { JournalLockedError, createJournalLease, } from "./execution-journal-loc
|
|
|
7
7
|
export { JournalLockedError, JournalLockCorruptionError } from "./execution-journal-lock.js";
|
|
8
8
|
const ExecutionIdSchema = z.string().uuid();
|
|
9
9
|
const TimestampSchema = z.string().datetime({ offset: true });
|
|
10
|
-
const RuntimeSchema = z.enum(["claude", "codex", "kimi"]);
|
|
10
|
+
const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode"]);
|
|
11
11
|
const RUNTIME_READY_SUFFIX = ".runtime-ready";
|
|
12
12
|
const RawJournalEntrySchema = z.object({
|
|
13
13
|
executionId: ExecutionIdSchema,
|
|
@@ -8,7 +8,7 @@ const MIN_SIGNED_32_INTEGER = -2_147_483_648;
|
|
|
8
8
|
const SequenceSchema = z.number().int().min(0).max(MAX_PG_INTEGER);
|
|
9
9
|
const TokenCountSchema = z.number().int().min(0).max(MAX_PG_INTEGER);
|
|
10
10
|
const ExitCodeSchema = z.number().int().min(MIN_SIGNED_32_INTEGER).max(MAX_PG_INTEGER);
|
|
11
|
-
const RuntimeSchema = z.enum(["claude", "codex", "kimi"]);
|
|
11
|
+
const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode"]);
|
|
12
12
|
const UnsafePathCharacterSchema = /[\p{Cc}<>:"/\\|?*]/u;
|
|
13
13
|
const WindowsReservedNameSchema = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
|
|
14
14
|
export const AgentHandleSchema = z.string().min(1).max(64).refine((handle) => handle !== "."
|
|
@@ -22,16 +22,8 @@ const ProjectSkillRefSchema = z.object({
|
|
|
22
22
|
projectId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/u),
|
|
23
23
|
skillName: z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/u),
|
|
24
24
|
}).strict();
|
|
25
|
-
export const ReasoningSchema = z.
|
|
26
|
-
"
|
|
27
|
-
"none",
|
|
28
|
-
"minimal",
|
|
29
|
-
"low",
|
|
30
|
-
"medium",
|
|
31
|
-
"high",
|
|
32
|
-
"xhigh",
|
|
33
|
-
"max",
|
|
34
|
-
]);
|
|
25
|
+
export const ReasoningSchema = z.string().min(1).max(64)
|
|
26
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u, "invalid runtime reasoning token");
|
|
35
27
|
export const PermissionSchema = z.enum([
|
|
36
28
|
"local_default",
|
|
37
29
|
"sandboxed",
|
package/dist/execution-runner.js
CHANGED
|
@@ -169,7 +169,10 @@ function validReasoning(spec) {
|
|
|
169
169
|
if (spec.runtime.name === "codex") {
|
|
170
170
|
return CODEX_EFFORT_LEVELS.includes(reasoning);
|
|
171
171
|
}
|
|
172
|
-
|
|
172
|
+
if (spec.runtime.name === "kimi") {
|
|
173
|
+
return KIMI_EFFORT_LEVELS.includes(reasoning);
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
173
176
|
}
|
|
174
177
|
function launchProviderConfig(config) {
|
|
175
178
|
if (config === undefined)
|
|
@@ -241,8 +244,14 @@ function admission(spec, config, dependencies, at) {
|
|
|
241
244
|
return { rejected: rejection(spec.executionId, "resource_limit", "Local execution capacity is exhausted", at) };
|
|
242
245
|
}
|
|
243
246
|
const permission = effectivePermission(spec, config);
|
|
244
|
-
if (spec.runtime.name === "kimi"
|
|
245
|
-
|
|
247
|
+
if ((spec.runtime.name === "kimi" || spec.runtime.name === "hermes" || spec.runtime.name === "opencode")
|
|
248
|
+
&& permission !== "full_access") {
|
|
249
|
+
const displayName = spec.runtime.name === "kimi"
|
|
250
|
+
? "Kimi"
|
|
251
|
+
: spec.runtime.name === "hermes" ? "Hermes" : "OpenCode";
|
|
252
|
+
return {
|
|
253
|
+
rejected: rejection(spec.executionId, "local_policy_denied", `${displayName} cannot enforce ${permission} permission`, at),
|
|
254
|
+
};
|
|
246
255
|
}
|
|
247
256
|
return { spec, permission };
|
|
248
257
|
}
|
|
@@ -404,7 +413,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
404
413
|
let recalledMemory = "";
|
|
405
414
|
if (spec.agent.memoryEnabled === true && dependencies.agentMemory !== undefined) {
|
|
406
415
|
try {
|
|
407
|
-
recalledMemory = await cancellable(dependencies.agentMemory.recall(spec.agent.handle, spec.instructions.wakePrompt), dependencies.cancellation);
|
|
416
|
+
recalledMemory = await cancellable(dependencies.agentMemory.recall(spec.agent.handle, spec.instructions.wakePrompt, spec.executionId), dependencies.cancellation);
|
|
408
417
|
}
|
|
409
418
|
catch (error) {
|
|
410
419
|
if (error instanceof ExecutionCancelledError)
|
package/dist/external-output.js
CHANGED
|
@@ -62,6 +62,10 @@ export function decodeExternalOutputEvent(runtime, event, decoder) {
|
|
|
62
62
|
? decoder.push(candidate.item.text)
|
|
63
63
|
: [];
|
|
64
64
|
}
|
|
65
|
+
if ((runtime === "hermes" && candidate.type === "hermes.acp.text_delta")
|
|
66
|
+
|| (runtime === "opencode" && candidate.type === "opencode.text_delta")) {
|
|
67
|
+
return typeof candidate.text === "string" ? decoder.push(candidate.text) : [];
|
|
68
|
+
}
|
|
65
69
|
return candidate.type === undefined
|
|
66
70
|
&& candidate.role === "assistant"
|
|
67
71
|
&& typeof candidate.content === "string"
|
package/dist/list-models.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { isWin } from "./platform.js";
|
|
4
|
+
import { listHermesModels } from "./runtimes/hermes-models.js";
|
|
4
5
|
const execFileRaw = promisify(execFile);
|
|
5
6
|
const MODEL_PROBE_TIMEOUT_MS = 8_000;
|
|
6
7
|
const MODEL_PROBE_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
@@ -11,14 +12,16 @@ const execFileP = (bin, args) => execFileRaw(bin, args, {
|
|
|
11
12
|
killSignal: "SIGKILL",
|
|
12
13
|
maxBuffer: MODEL_PROBE_MAX_BUFFER_BYTES,
|
|
13
14
|
});
|
|
14
|
-
export async function listRuntimeModels(runtime) {
|
|
15
|
+
export async function listRuntimeModels(runtime, options = {}) {
|
|
15
16
|
switch (runtime) {
|
|
16
17
|
case "codex":
|
|
17
18
|
return parseCodexModels((await execFileP("codex", ["debug", "models"])).stdout);
|
|
18
19
|
case "cursor":
|
|
19
20
|
return parseCursorModels((await execFileP("cursor-agent", ["--list-models"])).stdout);
|
|
21
|
+
case "hermes":
|
|
22
|
+
return listHermesModels(options);
|
|
20
23
|
case "opencode":
|
|
21
|
-
return
|
|
24
|
+
return listOpencodeModels();
|
|
22
25
|
case "pi":
|
|
23
26
|
return parsePiModels((await execFileP("pi", ["--list-models"])).stdout);
|
|
24
27
|
default:
|
|
@@ -76,12 +79,103 @@ function parseCursorModels(stdout) {
|
|
|
76
79
|
return { id: id.trim(), label: (label || id).trim(), ...(index === 0 ? { default: true } : {}) };
|
|
77
80
|
});
|
|
78
81
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
async function outputEvenOnFailure(bin, args) {
|
|
83
|
+
try {
|
|
84
|
+
return (await execFileP(bin, args)).stdout;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const stdout = error.stdout;
|
|
88
|
+
if (typeof stdout === "string")
|
|
89
|
+
return stdout;
|
|
90
|
+
if (Buffer.isBuffer(stdout))
|
|
91
|
+
return stdout.toString("utf8");
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function listOpencodeModels() {
|
|
96
|
+
const verbose = parseOpencodeModels(await outputEvenOnFailure("opencode", ["models", "--verbose"]));
|
|
97
|
+
if (verbose.length > 0)
|
|
98
|
+
return verbose;
|
|
99
|
+
return parseOpencodeModels(await outputEvenOnFailure("opencode", ["models"]));
|
|
100
|
+
}
|
|
101
|
+
const VARIANT_ORDER = new Map([
|
|
102
|
+
["none", 0], ["minimal", 1], ["low", 2], ["medium", 3],
|
|
103
|
+
["high", 4], ["xhigh", 5], ["max", 6],
|
|
104
|
+
]);
|
|
105
|
+
function modelIdLine(line) {
|
|
106
|
+
const id = line.trim().split(/\s+/, 1)[0] ?? "";
|
|
107
|
+
if (!id.includes("/") || /^["{[]/.test(id) || id === id.toUpperCase())
|
|
108
|
+
return null;
|
|
109
|
+
return id;
|
|
110
|
+
}
|
|
111
|
+
function collectJson(lines, start) {
|
|
112
|
+
let raw = "";
|
|
113
|
+
for (let index = start; index < lines.length; index += 1) {
|
|
114
|
+
if (index > start && modelIdLine(lines[index]) !== null)
|
|
115
|
+
return { raw, next: index };
|
|
116
|
+
raw += `${raw ? "\n" : ""}${lines[index]}`;
|
|
117
|
+
try {
|
|
118
|
+
JSON.parse(raw);
|
|
119
|
+
return { raw, next: index + 1 };
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// A pretty-printed block is complete only when JSON.parse succeeds.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { raw, next: lines.length };
|
|
126
|
+
}
|
|
127
|
+
function parseVariants(raw) {
|
|
128
|
+
try {
|
|
129
|
+
const meta = JSON.parse(raw);
|
|
130
|
+
const variants = meta.variants ?? {};
|
|
131
|
+
const looksReasoning = meta.reasoning === true || Object.entries(variants).some(([name, value]) => VARIANT_ORDER.has(name) || typeof value.reasoningEffort === "string" || value.thinking != null);
|
|
132
|
+
if (!looksReasoning)
|
|
133
|
+
return [];
|
|
134
|
+
return Object.entries(variants)
|
|
135
|
+
.filter(([name, value]) => name.length > 0 && value.disabled !== true)
|
|
136
|
+
.map(([name]) => name)
|
|
137
|
+
.sort((left, right) => {
|
|
138
|
+
const leftOrder = VARIANT_ORDER.get(left);
|
|
139
|
+
const rightOrder = VARIANT_ORDER.get(right);
|
|
140
|
+
if (leftOrder !== undefined && rightOrder !== undefined)
|
|
141
|
+
return leftOrder - rightOrder;
|
|
142
|
+
if (leftOrder !== undefined)
|
|
143
|
+
return -1;
|
|
144
|
+
if (rightOrder !== undefined)
|
|
145
|
+
return 1;
|
|
146
|
+
return left.localeCompare(right);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function parseOpencodeModels(stdout) {
|
|
154
|
+
const lines = stdout.split(/\r?\n/);
|
|
155
|
+
const models = [];
|
|
156
|
+
const byId = new Map();
|
|
157
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
158
|
+
const id = modelIdLine(lines[index]);
|
|
159
|
+
if (id === null)
|
|
160
|
+
continue;
|
|
161
|
+
let modelIndex = byId.get(id);
|
|
162
|
+
if (modelIndex === undefined) {
|
|
163
|
+
modelIndex = models.length;
|
|
164
|
+
byId.set(id, modelIndex);
|
|
165
|
+
models.push({ id, label: id, ...(modelIndex === 0 ? { default: true } : {}) });
|
|
166
|
+
}
|
|
167
|
+
let next = index + 1;
|
|
168
|
+
while (next < lines.length && lines[next].trim() === "")
|
|
169
|
+
next += 1;
|
|
170
|
+
if (next >= lines.length || !lines[next].trim().startsWith("{"))
|
|
171
|
+
continue;
|
|
172
|
+
const block = collectJson(lines, next);
|
|
173
|
+
const reasoning = parseVariants(block.raw);
|
|
174
|
+
if (reasoning.length > 0)
|
|
175
|
+
models[modelIndex] = { ...models[modelIndex], reasoning };
|
|
176
|
+
index = block.next - 1;
|
|
177
|
+
}
|
|
178
|
+
return models;
|
|
85
179
|
}
|
|
86
180
|
function parsePiModels(stdout) {
|
|
87
181
|
return stdout
|
package/dist/local-executor.js
CHANGED
|
@@ -19,6 +19,7 @@ import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancell
|
|
|
19
19
|
import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
|
|
20
20
|
import { dslog } from "./slog.js";
|
|
21
21
|
import { evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
|
|
22
|
+
import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
|
|
22
23
|
function memoryPruneSnapshotFields(snapshot) {
|
|
23
24
|
const fields = {};
|
|
24
25
|
for (const [label, fact] of Object.entries(snapshot)) {
|
|
@@ -79,6 +80,7 @@ export function withLocalExecutionFacts(serverPrompt, maxBytes) {
|
|
|
79
80
|
}
|
|
80
81
|
const STDERR_TAIL_CAP = 2_000;
|
|
81
82
|
const MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS = 2_000;
|
|
83
|
+
const LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS = 250;
|
|
82
84
|
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
83
85
|
const RESERVED_ENV = new Set([
|
|
84
86
|
"PATH",
|
|
@@ -112,15 +114,17 @@ export function awaitExit(child) {
|
|
|
112
114
|
});
|
|
113
115
|
}
|
|
114
116
|
export function exitActivity(runtime, exitCode, stderrTail) {
|
|
115
|
-
if ((runtime === "codex" || runtime === "kimi" ||
|
|
117
|
+
if ((runtime === "codex" || runtime === "kimi" || runtime === "hermes"
|
|
118
|
+
|| runtime === "opencode" || exitCode === -1) && exitCode !== 0) {
|
|
116
119
|
return {
|
|
117
120
|
kind: "error",
|
|
118
121
|
label: "运行出错",
|
|
119
122
|
detail: `${runtime} exited with code ${exitCode}${stderrTail ? `: ${stderrTail}` : ""}`,
|
|
120
123
|
};
|
|
121
124
|
}
|
|
122
|
-
if (runtime === "kimi" && exitCode === 0)
|
|
125
|
+
if ((runtime === "kimi" || runtime === "hermes" || runtime === "opencode") && exitCode === 0) {
|
|
123
126
|
return { kind: "done", label: "本轮结束" };
|
|
127
|
+
}
|
|
124
128
|
return null;
|
|
125
129
|
}
|
|
126
130
|
function wrapChild(child) {
|
|
@@ -222,16 +226,27 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
222
226
|
: "high"
|
|
223
227
|
: runtime.reasoning;
|
|
224
228
|
const providerFp = providerFingerprint(runtime.name, providerConfig);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
229
|
+
let workspace;
|
|
230
|
+
try {
|
|
231
|
+
workspace = await awaitWithCancellation(prepareWorkspace({
|
|
232
|
+
agentsRoot: input.launch.agentsRoot,
|
|
233
|
+
handle: input.handle,
|
|
234
|
+
cliPath: input.launch.cliPath,
|
|
235
|
+
executionId: input.executionId,
|
|
236
|
+
...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
|
|
237
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
238
|
+
...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
|
|
239
|
+
...(input.launch.description ? { description: input.launch.description } : {}),
|
|
240
|
+
}), dependencies.cancellation);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
logLocalMemoryContextPrepareFailure({
|
|
244
|
+
executionId: input.executionId,
|
|
245
|
+
agentHandle: input.handle,
|
|
246
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
247
|
+
}, "workspace_prepare", error);
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
235
250
|
let materialized = null;
|
|
236
251
|
let knownAttachmentDirectory = null;
|
|
237
252
|
let startupReservation = null;
|
|
@@ -242,6 +257,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
242
257
|
let memoryPruneBeforeSnapshot = null;
|
|
243
258
|
let memoryPruneSharedWriteKey = null;
|
|
244
259
|
let executionWorkspace = workspace;
|
|
260
|
+
let localMemoryTelemetry = null;
|
|
245
261
|
try {
|
|
246
262
|
if (isDeepSeekCodex && !providerConfig.providerApiKey) {
|
|
247
263
|
throw new Error("DeepSeek API key is not configured for this Agent");
|
|
@@ -345,6 +361,29 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
345
361
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
346
362
|
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
347
363
|
memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
|
|
364
|
+
try {
|
|
365
|
+
localMemoryTelemetry = createLocalMemoryTelemetry({
|
|
366
|
+
executionId: input.executionId,
|
|
367
|
+
agentHandle: input.handle,
|
|
368
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
369
|
+
runtime: runtime.name,
|
|
370
|
+
stableProtocolRuntime: dependencies.launchRuntime !== undefined,
|
|
371
|
+
agentDir: executionWorkspace.dir,
|
|
372
|
+
runDir: executionWorkspace.runDir,
|
|
373
|
+
isMemoryPrune: memoryPruneTraceId !== null,
|
|
374
|
+
diagnosticsTimeoutMs: dependencies.localMemoryDiagnosticsTimeoutMs
|
|
375
|
+
?? LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS,
|
|
376
|
+
});
|
|
377
|
+
await localMemoryTelemetry.captureBefore();
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
localMemoryTelemetry = null;
|
|
381
|
+
logLocalMemoryDiagnosticsFailure({
|
|
382
|
+
executionId: input.executionId,
|
|
383
|
+
agentHandle: input.handle,
|
|
384
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
385
|
+
}, "initialize", error);
|
|
386
|
+
}
|
|
348
387
|
if (memoryPruneTraceId !== null) {
|
|
349
388
|
memoryPruneSharedWriteKey = JSON.stringify([input.launch.agentsRoot, input.handle]);
|
|
350
389
|
const activePruneCount = (activeMemoryPrunes.get(memoryPruneSharedWriteKey) ?? 0) + 1;
|
|
@@ -378,7 +417,25 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
378
417
|
}
|
|
379
418
|
}
|
|
380
419
|
memoryPruneFailurePhase = "prompt_write";
|
|
381
|
-
|
|
420
|
+
try {
|
|
421
|
+
await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
logLocalMemoryContextPrepareFailure({
|
|
425
|
+
executionId: input.executionId,
|
|
426
|
+
agentHandle: input.handle,
|
|
427
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
428
|
+
}, "system_prompt_write", error);
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
localMemoryTelemetry?.contextPrepared({
|
|
432
|
+
systemPrompt,
|
|
433
|
+
memory: executionWorkspace.memory,
|
|
434
|
+
resumed: resuming,
|
|
435
|
+
...(executionWorkspace.memorySeedCreated === undefined
|
|
436
|
+
? {}
|
|
437
|
+
: { memorySeedCreated: executionWorkspace.memorySeedCreated }),
|
|
438
|
+
});
|
|
382
439
|
memoryPruneFailurePhase = "runtime_prepare";
|
|
383
440
|
const inheritedEnv = { ...process.env };
|
|
384
441
|
for (const key of Object.keys(inheritedEnv)) {
|
|
@@ -454,6 +511,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
454
511
|
const child = input.projectSkills !== undefined && dependencies.projectSkills !== undefined
|
|
455
512
|
? await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, () => launchRuntime(launchRequest))
|
|
456
513
|
: await launchRuntime(launchRequest);
|
|
514
|
+
localMemoryTelemetry?.markRuntimeStarted();
|
|
457
515
|
memoryPruneFailurePhase = "runtime_execution";
|
|
458
516
|
if (child.cancel !== undefined) {
|
|
459
517
|
dependencies.cancellation?.register(child.cancel);
|
|
@@ -479,6 +537,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
479
537
|
if (runtimeReady)
|
|
480
538
|
return;
|
|
481
539
|
runtimeReady = true;
|
|
540
|
+
localMemoryTelemetry?.markRuntimeReady();
|
|
482
541
|
startupReservation?.release();
|
|
483
542
|
dslog("runtime.start_ready", "runtime 已完成初始化", {
|
|
484
543
|
execution_id: input.executionId,
|
|
@@ -493,6 +552,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
493
552
|
const event = parseLine(line);
|
|
494
553
|
if (!event)
|
|
495
554
|
return;
|
|
555
|
+
localMemoryTelemetry?.observe(event);
|
|
496
556
|
if (isRuntimeReadyEvent(runtime.name, event))
|
|
497
557
|
markRuntimeReady();
|
|
498
558
|
const meta = extractRunMeta(event);
|
|
@@ -511,7 +571,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
511
571
|
const extracted = extractFinalText(event);
|
|
512
572
|
if (extracted) {
|
|
513
573
|
const incremental = typeof event === "object" && event !== null
|
|
514
|
-
&& "type" in event && event.type === "kimi.acp.text_delta"
|
|
574
|
+
&& "type" in event && (event.type === "kimi.acp.text_delta"
|
|
575
|
+
|| event.type === "hermes.acp.text_delta" || event.type === "opencode.text_delta");
|
|
515
576
|
finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
|
|
516
577
|
}
|
|
517
578
|
for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
|
|
@@ -569,6 +630,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
569
630
|
throw error;
|
|
570
631
|
}
|
|
571
632
|
const { exitCode, spawnError, terminationSignal } = runtimeExit;
|
|
633
|
+
localMemoryTelemetry?.markRuntimeExited(exitCode);
|
|
572
634
|
memoryPruneRuntimeExitCode = exitCode;
|
|
573
635
|
const errorTail = [
|
|
574
636
|
stderrTail.trim(),
|
|
@@ -602,6 +664,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
602
664
|
}
|
|
603
665
|
}
|
|
604
666
|
memoryPruneExecutorCompleted = true;
|
|
667
|
+
localMemoryTelemetry?.markExecutorCompleted();
|
|
605
668
|
return {
|
|
606
669
|
workspaceRunDir: executionWorkspace.runDir,
|
|
607
670
|
exitCode,
|
|
@@ -624,6 +687,16 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
624
687
|
}
|
|
625
688
|
finally {
|
|
626
689
|
startupReservation?.release();
|
|
690
|
+
try {
|
|
691
|
+
await localMemoryTelemetry?.finish();
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
logLocalMemoryDiagnosticsFailure({
|
|
695
|
+
executionId: input.executionId,
|
|
696
|
+
agentHandle: input.handle,
|
|
697
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
698
|
+
}, "finish", error);
|
|
699
|
+
}
|
|
627
700
|
if (memoryPruneTraceId !== null) {
|
|
628
701
|
try {
|
|
629
702
|
const snapshot = await inspectMemoryPruneFilesWithinDeadline(executionWorkspace.dir, executionWorkspace.runDir, executionWorkspace.workLogPath, dependencies.memoryPruneDiagnosticsTimeoutMs
|