@zq-silk/yui 0.6.2 → 0.6.4
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/ARCHITECTURE.md +28 -4
- package/README.md +60 -97
- package/dist/agent/argumentPolicy.js +1 -1
- package/dist/agent/managedRuntimeEnvironment.js +1 -0
- package/dist/cli/commandCatalog.js +19 -9
- package/dist/cli/interactionPolicy.js +4 -2
- package/dist/cli.js +77 -32
- package/dist/commands/taskCommands.js +46 -11
- package/dist/commands/taskContextCommand.js +1 -1
- package/dist/commands/taskRoleRuntimeStatus.js +170 -10
- package/dist/controller/agentRuntimeObserver.js +210 -0
- package/dist/controller/clientRuntime.js +3 -21
- package/dist/controller/controller.js +47 -7
- package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
- package/dist/controller/runtime.js +9 -3
- package/dist/controller/runtimeEventInbox.js +49 -295
- package/dist/controller/runtimeEventProcessor.js +184 -321
- package/dist/controller/runtimeHookRunFence.js +226 -0
- package/dist/controller/runtimeLaunchCoordinator.js +91 -26
- package/dist/controller/runtimeObservationHook.js +112 -0
- package/dist/core/controllerServer.js +5 -0
- package/dist/executor/agentAdapter.js +18 -3
- package/dist/executor/fileRoleLaunchPlanner.js +64 -15
- package/dist/executor/managedClaudeRunner.js +121 -0
- package/dist/observability/executionAudit.js +6 -3
- package/dist/repository/taskWorkspacePreparer.js +1 -4
- package/dist/run/providerRetryConfig.js +8 -3
- package/dist/runtime/agentDriver.js +229 -0
- package/dist/runtime/agentDriverObservation.js +57 -0
- package/dist/runtime/builtinAgentDrivers.js +235 -0
- package/dist/runtime/builtinTranscriptObserver.js +290 -0
- package/dist/runtime/builtinTranscriptUsage.js +97 -0
- package/dist/runtime/exactControlPlane.js +2 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/ports.js +12 -1
- package/dist/runtime/runtimeObservation.js +297 -0
- package/dist/runtime/runtimeProjection.js +277 -0
- package/dist/runtime/sessionTerminationGuard.js +78 -22
- package/dist/runtime/tmuxAdapters.js +35 -0
- package/dist/scheduler/activeRoleRunDelivery.js +28 -13
- package/dist/scheduler/leaderWakeupProcessor.js +21 -2
- package/dist/scheduler/roleRunLiveness.js +2 -2
- package/dist/scheduler/roleRunStall.js +62 -114
- package/dist/storage/migration/productionRegistry.js +85 -0
- package/dist/storage/sqliteStore.js +3 -3
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +2 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +123 -0
- package/dist/telemetry/sqliteTelemetryStore.js +0 -28
- package/dist/telemetry/telemetryCompaction.js +1 -0
- package/dist/telemetry/telemetryConfig.js +4 -5
- package/dist/tmux/tmuxManager.js +136 -22
- package/dist/web/assets/client/view.js +1 -1
- package/dist/web/tmuxWebTerminal.js +17 -12
- package/dist/web/webSnapshot.js +1 -1
- package/dist/worktree/managedWorkspace.js +14 -0
- package/i18n/README.zh-CN.md +12 -7
- package/package.json +1 -1
- package/dist/controller/claudeLifecycleHook.js +0 -203
- package/dist/controller/codexLifecycleHook.js +0 -108
- package/dist/controller/providerHookRunFence.js +0 -156
- package/dist/lifecycle/providerLifecycleMapping.js +0 -190
- package/dist/telemetry/telemetryRouter.js +0 -32
|
@@ -19,6 +19,8 @@ import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactT
|
|
|
19
19
|
import { readActiveReleasePointer } from "../release/runtimeRelease.js";
|
|
20
20
|
import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
|
|
21
21
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
22
|
+
import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
23
|
+
import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
|
|
22
24
|
/** Builds managed native Agent launches from the authoritative Task records. */
|
|
23
25
|
export class FileRoleLaunchPlanner {
|
|
24
26
|
home;
|
|
@@ -304,14 +306,24 @@ export class FileRoleLaunchPlanner {
|
|
|
304
306
|
trustWorkspace: true
|
|
305
307
|
})
|
|
306
308
|
: undefined;
|
|
307
|
-
if (codexConfig?.notify.status === "configured"
|
|
309
|
+
if (codexConfig?.notify.status === "configured"
|
|
310
|
+
&& (owner.scope !== "task" || input.runId === undefined)) {
|
|
308
311
|
throw new Error("Codex notify is already configured by "
|
|
309
|
-
+ `${codexConfig.notify.source}; Yui requires exclusive
|
|
310
|
-
+ "notify callback and refuses to replace or be replaced
|
|
312
|
+
+ `${codexConfig.notify.source}; this interactive Yui Session requires exclusive `
|
|
313
|
+
+ "ownership of the structured notify callback and refuses to replace or be replaced "
|
|
314
|
+
+ "by native configuration.");
|
|
311
315
|
}
|
|
312
316
|
const managedRun = owner.scope === "task" && input.runId !== undefined
|
|
313
317
|
? this.store.getAgentRun(owner.taskId, input.runId)
|
|
314
318
|
: null;
|
|
319
|
+
const driver = builtinAgentDriverRegistry().require(builtinDriverIdForAdapter(configured.adapterId));
|
|
320
|
+
if (owner.scope === "task" && input.runId !== undefined) {
|
|
321
|
+
const admission = managedRuntimeAdmission(driver.capabilities);
|
|
322
|
+
if (!admission.admitted) {
|
|
323
|
+
throw new Error(`Agent Driver ${driver.id} cannot host managed Runs; missing capabilities: `
|
|
324
|
+
+ admission.missing.join(", "));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
315
327
|
const roleConfig = binding.config.adapterId === "claude"
|
|
316
328
|
&& owner.scope === "task"
|
|
317
329
|
&& input.runId !== undefined
|
|
@@ -352,9 +364,15 @@ export class FileRoleLaunchPlanner {
|
|
|
352
364
|
this.#resourceRegistrar().registerSessionContext(sessionContext.managedContextFile, { home: resolve(this.home), taskId: owner.taskId, basis: "descriptor" });
|
|
353
365
|
}
|
|
354
366
|
let args = [...compiled.argv];
|
|
367
|
+
let command = configured.command;
|
|
355
368
|
let session;
|
|
356
369
|
if (binding.adapterId === "codex") {
|
|
357
|
-
|
|
370
|
+
// Global/interactive Codex sessions still use notify for presentation.
|
|
371
|
+
// Managed Runs use the structured Driver Hook as their sole lifecycle
|
|
372
|
+
// authority, avoiding two terminal channels for the same Turn.
|
|
373
|
+
if (owner.scope !== "task" || input.runId === undefined) {
|
|
374
|
+
args = addCodexSessionNotify(args, launchMode, this.#cliPath);
|
|
375
|
+
}
|
|
358
376
|
// A fresh Codex TUI has no provider event before its first prompt. Carry
|
|
359
377
|
// the exact Run input as the provider's positional launch prompt so it is
|
|
360
378
|
// submitted only after Codex completes startup; never race terminal bytes
|
|
@@ -374,6 +392,7 @@ export class FileRoleLaunchPlanner {
|
|
|
374
392
|
}
|
|
375
393
|
else if (launchMode === "new") {
|
|
376
394
|
if (owner.scope === "task" && input.runId !== undefined) {
|
|
395
|
+
args.push("-p", "--output-format", "stream-json", "--input-format", "stream-json", "--verbose");
|
|
377
396
|
args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
|
|
378
397
|
}
|
|
379
398
|
const nativeSessionId = requireText(input.launchId === undefined
|
|
@@ -384,10 +403,25 @@ export class FileRoleLaunchPlanner {
|
|
|
384
403
|
}
|
|
385
404
|
else {
|
|
386
405
|
if (owner.scope === "task" && input.runId !== undefined) {
|
|
406
|
+
args.push("-p", "--output-format", "stream-json", "--input-format", "stream-json", "--verbose");
|
|
387
407
|
args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
|
|
388
408
|
}
|
|
389
409
|
session = readySession(input.agentId, binding.adapterId, resumeNativeSessionId, effective);
|
|
390
410
|
}
|
|
411
|
+
const managedClaudeRun = binding.adapterId === "claude"
|
|
412
|
+
&& owner.scope === "task"
|
|
413
|
+
&& input.runId !== undefined;
|
|
414
|
+
if (managedClaudeRun) {
|
|
415
|
+
command = process.execPath;
|
|
416
|
+
args = [
|
|
417
|
+
this.#cliPath,
|
|
418
|
+
"internal",
|
|
419
|
+
"managed-claude-run",
|
|
420
|
+
"--",
|
|
421
|
+
configured.command,
|
|
422
|
+
...args
|
|
423
|
+
];
|
|
424
|
+
}
|
|
391
425
|
const runtimeDescriptor = owner.scope === "task"
|
|
392
426
|
? createExactTaskRuntimeDescriptor({
|
|
393
427
|
controlPlaneDigest: exactControlPlaneDigest(this.#controlPlane),
|
|
@@ -418,7 +452,7 @@ export class FileRoleLaunchPlanner {
|
|
|
418
452
|
jobCallerKey = randomBytes(32).toString("hex");
|
|
419
453
|
}
|
|
420
454
|
const launch = {
|
|
421
|
-
command
|
|
455
|
+
command,
|
|
422
456
|
args,
|
|
423
457
|
env: {
|
|
424
458
|
...launchEnvironment,
|
|
@@ -428,6 +462,7 @@ export class FileRoleLaunchPlanner {
|
|
|
428
462
|
YUI_ROLE: role.name,
|
|
429
463
|
YUI_AGENT_ID: configured.id,
|
|
430
464
|
YUI_ADAPTER_ID: configured.adapterId,
|
|
465
|
+
YUI_DRIVER_ID: driver.id,
|
|
431
466
|
YUI_WORKSPACE: effectiveWorkspace,
|
|
432
467
|
...(jobCallerKey === undefined ? {} : { YUI_JOB_CALLER_KEY: jobCallerKey }),
|
|
433
468
|
...(runtimeDescriptor === undefined
|
|
@@ -468,7 +503,7 @@ export class FileRoleLaunchPlanner {
|
|
|
468
503
|
},
|
|
469
504
|
launch: scopedLaunch,
|
|
470
505
|
session,
|
|
471
|
-
...(binding.adapterId === "codex" && input.runId !== undefined
|
|
506
|
+
...((binding.adapterId === "codex" || managedClaudeRun) && input.runId !== undefined
|
|
472
507
|
? { initialPromptRunId: input.runId }
|
|
473
508
|
: {})
|
|
474
509
|
};
|
|
@@ -570,19 +605,24 @@ function ensureManagedClaudeLifecyclePlugin(home, cliPath) {
|
|
|
570
605
|
const command = {
|
|
571
606
|
type: "command",
|
|
572
607
|
command: canonicalPath(process.execPath),
|
|
573
|
-
args: [canonicalPath(cliPath), "internal", "
|
|
608
|
+
args: [canonicalPath(cliPath), "internal", "runtime-hook"]
|
|
574
609
|
};
|
|
575
610
|
writeTextFileAtomically(join(root, "hooks", "hooks.json"), `${JSON.stringify({
|
|
576
611
|
hooks: {
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
// fence; PostToolUse is a provider-native in-turn progress fact and
|
|
580
|
-
// StopFailure is the terminal failure fact. All route to the same
|
|
581
|
-
// Yui-owned entrypoint, which parses by hook_event_name.
|
|
612
|
+
// All native names terminate at the Driver edge; core sees only
|
|
613
|
+
// canonical RuntimeObservation values.
|
|
582
614
|
SessionStart: [{ hooks: [command] }],
|
|
583
615
|
UserPromptSubmit: [{ hooks: [command] }],
|
|
616
|
+
PreToolUse: [{ hooks: [command] }],
|
|
617
|
+
PermissionRequest: [{ hooks: [command] }],
|
|
618
|
+
MessageDisplay: [{ hooks: [command] }],
|
|
584
619
|
PostToolUse: [{ hooks: [command] }],
|
|
585
|
-
|
|
620
|
+
PostToolUseFailure: [{ hooks: [command] }],
|
|
621
|
+
SubagentStart: [{ hooks: [command] }],
|
|
622
|
+
SubagentStop: [{ hooks: [command] }],
|
|
623
|
+
Stop: [{ hooks: [command] }],
|
|
624
|
+
StopFailure: [{ hooks: [command] }],
|
|
625
|
+
SessionEnd: [{ hooks: [command] }]
|
|
586
626
|
}
|
|
587
627
|
}, null, 2)}\n`);
|
|
588
628
|
return root;
|
|
@@ -669,10 +709,19 @@ function codexLifecycleHooksConfig(cliPath) {
|
|
|
669
709
|
shellQuote(canonicalPath(process.execPath)),
|
|
670
710
|
shellQuote(canonicalPath(cliPath)),
|
|
671
711
|
"internal",
|
|
672
|
-
"
|
|
712
|
+
"runtime-hook"
|
|
673
713
|
].join(" ");
|
|
674
714
|
const handler = `{hooks=[{type="command",command=${JSON.stringify(command)}}]}`;
|
|
675
|
-
return `hooks={
|
|
715
|
+
return `hooks={`
|
|
716
|
+
+ `SessionStart=[${handler}],`
|
|
717
|
+
+ `UserPromptSubmit=[${handler}],`
|
|
718
|
+
+ `PreToolUse=[${handler}],`
|
|
719
|
+
+ `PermissionRequest=[${handler}],`
|
|
720
|
+
+ `PostToolUse=[${handler}],`
|
|
721
|
+
+ `SubagentStart=[${handler}],`
|
|
722
|
+
+ `SubagentStop=[${handler}],`
|
|
723
|
+
+ `Stop=[${handler}]`
|
|
724
|
+
+ `}`;
|
|
676
725
|
}
|
|
677
726
|
function addCodexLifecycleHooks(args, mode, cliPath) {
|
|
678
727
|
// Session flags are Yui-owned and exact to this launch. Hook trust bypass is
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { constants } from "node:os";
|
|
3
|
+
const SIGKILL_GRACE_MS = 2_000;
|
|
4
|
+
/** The documented Claude Code stream-json user-message envelope. */
|
|
5
|
+
export function buildManagedClaudeInput(prompt) {
|
|
6
|
+
if (typeof prompt !== "string" || prompt.includes("\0")) {
|
|
7
|
+
throw new TypeError("Managed Claude prompt must be text without NUL bytes.");
|
|
8
|
+
}
|
|
9
|
+
return `${JSON.stringify({
|
|
10
|
+
type: "user",
|
|
11
|
+
message: {
|
|
12
|
+
role: "user",
|
|
13
|
+
content: [{ type: "text", text: prompt }]
|
|
14
|
+
}
|
|
15
|
+
})}\n`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs one autonomous Claude turn without exposing the prompt in argv or
|
|
19
|
+
* depending on terminal readiness/key timing. stdout/stderr are connected
|
|
20
|
+
* before stdin is written, so a provider startup burst cannot deadlock the
|
|
21
|
+
* prompt pipe. EOF makes this a finite per-Run process; native continuity is
|
|
22
|
+
* carried by Claude's --session-id/--resume arguments.
|
|
23
|
+
*/
|
|
24
|
+
export async function runManagedClaudeProcess(input) {
|
|
25
|
+
const command = requireProcessToken(input.command, "Managed Claude command");
|
|
26
|
+
const args = input.args.map((value) => requireProcessToken(value, "Managed Claude argument"));
|
|
27
|
+
const child = spawn(command, args, {
|
|
28
|
+
...(input.cwd === undefined ? {} : { cwd: input.cwd }),
|
|
29
|
+
env: input.environment ?? process.env,
|
|
30
|
+
stdio: [
|
|
31
|
+
"pipe",
|
|
32
|
+
input.stdout === undefined ? "inherit" : "pipe",
|
|
33
|
+
input.stderr === undefined ? "inherit" : "pipe"
|
|
34
|
+
],
|
|
35
|
+
// Linux-only package: isolate the Provider process tree so tmux/session
|
|
36
|
+
// cancellation cannot leave Claude tool descendants behind.
|
|
37
|
+
detached: true
|
|
38
|
+
});
|
|
39
|
+
if (child.stdin === null) {
|
|
40
|
+
child.kill();
|
|
41
|
+
throw new Error("Managed Claude stdin pipe is unavailable.");
|
|
42
|
+
}
|
|
43
|
+
if (input.stdout !== undefined)
|
|
44
|
+
child.stdout.pipe(input.stdout, { end: false });
|
|
45
|
+
if (input.stderr !== undefined)
|
|
46
|
+
child.stderr.pipe(input.stderr, { end: false });
|
|
47
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
48
|
+
const handlers = new Map();
|
|
49
|
+
let killTimer;
|
|
50
|
+
let terminationRequested = false;
|
|
51
|
+
let childSettled = false;
|
|
52
|
+
const signalProcessGroup = (signal) => {
|
|
53
|
+
if (child.pid === undefined || child.exitCode !== null || child.signalCode !== null)
|
|
54
|
+
return;
|
|
55
|
+
try {
|
|
56
|
+
process.kill(-child.pid, signal);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code !== "ESRCH")
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const terminateProcessGroup = (signal) => {
|
|
64
|
+
if (terminationRequested)
|
|
65
|
+
return;
|
|
66
|
+
terminationRequested = true;
|
|
67
|
+
signalProcessGroup(signal);
|
|
68
|
+
killTimer = setTimeout(() => signalProcessGroup("SIGKILL"), SIGKILL_GRACE_MS);
|
|
69
|
+
killTimer.unref();
|
|
70
|
+
};
|
|
71
|
+
for (const signal of signals) {
|
|
72
|
+
const handler = () => terminateProcessGroup(signal);
|
|
73
|
+
handlers.set(signal, handler);
|
|
74
|
+
process.on(signal, handler);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const closed = new Promise((resolve, reject) => {
|
|
78
|
+
child.once("error", (error) => {
|
|
79
|
+
childSettled = true;
|
|
80
|
+
reject(error);
|
|
81
|
+
});
|
|
82
|
+
child.once("close", (code, signal) => {
|
|
83
|
+
childSettled = true;
|
|
84
|
+
resolve({ code, signal });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
await new Promise((resolve, reject) => {
|
|
89
|
+
child.stdin.once("error", reject);
|
|
90
|
+
child.stdin.end(buildManagedClaudeInput(input.prompt), "utf8", resolve);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
// A failed prompt pipe is not permission to orphan a Provider process or
|
|
95
|
+
// its tools. Terminate the whole group and wait for bounded convergence.
|
|
96
|
+
if (!childSettled) {
|
|
97
|
+
terminateProcessGroup("SIGTERM");
|
|
98
|
+
await closed.catch(() => undefined);
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
const result = await closed;
|
|
103
|
+
return result.code ?? (result.signal === null ? 1 : signalExitCode(result.signal));
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
if (killTimer !== undefined)
|
|
107
|
+
clearTimeout(killTimer);
|
|
108
|
+
for (const [signal, handler] of handlers)
|
|
109
|
+
process.removeListener(signal, handler);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function requireProcessToken(value, label) {
|
|
113
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
114
|
+
throw new TypeError(`${label} must be non-empty text without NUL bytes.`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
function signalExitCode(signal) {
|
|
119
|
+
const number = constants.signals[signal];
|
|
120
|
+
return number === undefined ? 1 : 128 + number;
|
|
121
|
+
}
|
|
@@ -14,6 +14,7 @@ import { readdirSync, statSync } from "node:fs";
|
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
16
16
|
import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
|
|
17
|
+
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
17
18
|
import { classifyAgentRunFailure, classifyIntegrationAttempt, classifyReviewRound, classifyWakeReasons, countFaultClasses } from "./faultClassification.js";
|
|
18
19
|
import { UNSUPPORTED } from "./runtimeIdentity.js";
|
|
19
20
|
export function createProductionExecutionAuditPorts() {
|
|
@@ -289,9 +290,10 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
289
290
|
continue;
|
|
290
291
|
if (event.type === "runtime.role-session-reset")
|
|
291
292
|
resets += 1;
|
|
292
|
-
else if (event.
|
|
293
|
+
else if (runtimeObservationFromTaskEvent(event)?.kind.startsWith("session.")) {
|
|
293
294
|
lifecycleEvents += 1;
|
|
294
|
-
|
|
295
|
+
}
|
|
296
|
+
else if (event.type === "runtime.turn-failed")
|
|
295
297
|
stopFailures += 1;
|
|
296
298
|
}
|
|
297
299
|
}
|
|
@@ -420,8 +422,9 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
420
422
|
if (!inWindow(event.createdAt, options))
|
|
421
423
|
continue;
|
|
422
424
|
total += 1;
|
|
423
|
-
if (event
|
|
425
|
+
if (runtimeObservationFromTaskEvent(event)?.kind === "activity.observed") {
|
|
424
426
|
progressEvents += 1;
|
|
427
|
+
}
|
|
425
428
|
else if (event.type === "runtime.event-obsolete")
|
|
426
429
|
obsoleteEvents += 1;
|
|
427
430
|
}
|
|
@@ -8,7 +8,7 @@ import { attachReviewRoundWorkspace, recordReviewWorkspaceDisposition } from "..
|
|
|
8
8
|
import { StorageConflictError } from "../storage/taskStore.js";
|
|
9
9
|
import { bindTaskWorkspaceIdentity } from "../task/task.js";
|
|
10
10
|
import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
|
|
11
|
-
import { createManagedWorkspace, managedWorkspaceKey, managedWorktreeName } from "../worktree/managedWorkspace.js";
|
|
11
|
+
import { createManagedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
12
12
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
13
13
|
import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
|
|
14
14
|
import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
@@ -2542,9 +2542,6 @@ function sameEffectiveWorkspace(effective, workspace) {
|
|
|
2542
2542
|
function sameManagedWorkspace(left, right) {
|
|
2543
2543
|
return isDeepStrictEqual(left, right);
|
|
2544
2544
|
}
|
|
2545
|
-
function sameManagedWorkspaceIdentity(left, right) {
|
|
2546
|
-
return isDeepStrictEqual({ owner: left.owner, root: left.root, entries: left.entries }, { owner: right.owner, root: right.root, entries: right.entries });
|
|
2547
|
-
}
|
|
2548
2545
|
function isMissingPath(error) {
|
|
2549
2546
|
return typeof error === "object" && error !== null && "code" in error
|
|
2550
2547
|
&& error.code === "ENOENT";
|
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
import { supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
|
|
2
2
|
function parseAdapters(value) {
|
|
3
3
|
if (value === undefined)
|
|
4
4
|
return [];
|
|
5
|
+
const supported = supportedAgentAdapterIds();
|
|
6
|
+
const supportedSet = new Set(supported);
|
|
5
7
|
const adapters = [];
|
|
6
8
|
for (const raw of value.split(",")) {
|
|
7
9
|
const token = raw.trim().toLowerCase();
|
|
8
10
|
if (token === "")
|
|
9
11
|
continue;
|
|
10
12
|
if (token === "all") {
|
|
11
|
-
for (const adapter of
|
|
13
|
+
for (const adapter of supported) {
|
|
12
14
|
if (!adapters.includes(adapter))
|
|
13
15
|
adapters.push(adapter);
|
|
14
16
|
}
|
|
15
17
|
continue;
|
|
16
18
|
}
|
|
17
|
-
if (
|
|
19
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/u.test(token)) {
|
|
20
|
+
throw new Error(`Invalid Provider retry adapter: ${token}.`);
|
|
21
|
+
}
|
|
22
|
+
if (!supportedSet.has(token)) {
|
|
18
23
|
throw new Error(`Unknown Provider retry adapter: ${token}.`);
|
|
19
24
|
}
|
|
20
25
|
if (!adapters.includes(token)) {
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const SURFACES = ["managed-protocol", "interactive-cli"];
|
|
2
|
+
const OPERATIONS = ["model", "tool", "subagent"];
|
|
3
|
+
const WAIT_REASONS = ["user", "permission", "external"];
|
|
4
|
+
const USAGE_MODES = [
|
|
5
|
+
"streaming-cumulative",
|
|
6
|
+
"event-snapshot",
|
|
7
|
+
"terminal-cumulative",
|
|
8
|
+
"unavailable"
|
|
9
|
+
];
|
|
10
|
+
const DELIVERY_MODES = [
|
|
11
|
+
"ordered-replayable",
|
|
12
|
+
"ordered-best-effort",
|
|
13
|
+
"best-effort",
|
|
14
|
+
"host-only"
|
|
15
|
+
];
|
|
16
|
+
export function validateAgentDriverCapabilities(input) {
|
|
17
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
18
|
+
throw new Error("Agent Driver capabilities must be an object.");
|
|
19
|
+
}
|
|
20
|
+
const surfaces = uniqueMembers(input.surfaces, SURFACES, "Agent Driver surface");
|
|
21
|
+
if (surfaces.length === 0)
|
|
22
|
+
throw new Error("Agent Driver must expose at least one surface.");
|
|
23
|
+
const control = input.control;
|
|
24
|
+
if (control === null || typeof control !== "object" || Array.isArray(control)) {
|
|
25
|
+
throw new Error("Agent Driver control capabilities must be an object.");
|
|
26
|
+
}
|
|
27
|
+
for (const name of ["start", "resume", "sendTurn", "interrupt", "stop"]) {
|
|
28
|
+
if (typeof control[name] !== "boolean") {
|
|
29
|
+
throw new Error(`Agent Driver control capability ${name} must be boolean.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const observation = input.observation;
|
|
33
|
+
if (observation === null || typeof observation !== "object" || Array.isArray(observation)) {
|
|
34
|
+
throw new Error("Agent Driver observation capabilities must be an object.");
|
|
35
|
+
}
|
|
36
|
+
if (observation.sessionIdentity !== "exact" && observation.sessionIdentity !== "unavailable") {
|
|
37
|
+
throw new Error("Agent Driver session identity capability is invalid.");
|
|
38
|
+
}
|
|
39
|
+
if (observation.sessionBootstrap !== "preallocated"
|
|
40
|
+
&& observation.sessionBootstrap !== "discovered") {
|
|
41
|
+
throw new Error("Agent Driver Session bootstrap capability is invalid.");
|
|
42
|
+
}
|
|
43
|
+
if (observation.preInputReadiness !== "exact"
|
|
44
|
+
&& observation.preInputReadiness !== "unavailable") {
|
|
45
|
+
throw new Error("Agent Driver pre-input readiness capability is invalid.");
|
|
46
|
+
}
|
|
47
|
+
if (observation.promptAcceptance !== "exact" && observation.promptAcceptance !== "unavailable") {
|
|
48
|
+
throw new Error("Agent Driver prompt acceptance capability is invalid.");
|
|
49
|
+
}
|
|
50
|
+
if (!["exact", "partial", "unavailable"].includes(observation.turnLifecycle)) {
|
|
51
|
+
throw new Error("Agent Driver turn lifecycle capability is invalid.");
|
|
52
|
+
}
|
|
53
|
+
if (!USAGE_MODES.includes(observation.usage)) {
|
|
54
|
+
throw new Error("Agent Driver usage capability is invalid.");
|
|
55
|
+
}
|
|
56
|
+
if (!DELIVERY_MODES.includes(observation.delivery)) {
|
|
57
|
+
throw new Error("Agent Driver delivery capability is invalid.");
|
|
58
|
+
}
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
surfaces: Object.freeze(surfaces),
|
|
61
|
+
control: Object.freeze({
|
|
62
|
+
start: control.start,
|
|
63
|
+
resume: control.resume,
|
|
64
|
+
sendTurn: control.sendTurn,
|
|
65
|
+
interrupt: control.interrupt,
|
|
66
|
+
stop: control.stop
|
|
67
|
+
}),
|
|
68
|
+
observation: Object.freeze({
|
|
69
|
+
sessionIdentity: observation.sessionIdentity,
|
|
70
|
+
sessionBootstrap: observation.sessionBootstrap,
|
|
71
|
+
preInputReadiness: observation.preInputReadiness,
|
|
72
|
+
promptAcceptance: observation.promptAcceptance,
|
|
73
|
+
turnLifecycle: observation.turnLifecycle,
|
|
74
|
+
operations: Object.freeze(uniqueMembers(observation.operations, OPERATIONS, "Agent Driver operation")),
|
|
75
|
+
waiting: Object.freeze(uniqueMembers(observation.waiting, WAIT_REASONS, "Agent Driver wait reason")),
|
|
76
|
+
usage: observation.usage,
|
|
77
|
+
delivery: observation.delivery
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function managedRuntimeAdmission(capabilities) {
|
|
82
|
+
const actual = validateAgentDriverCapabilities(capabilities);
|
|
83
|
+
const missing = [];
|
|
84
|
+
if (!actual.control.start)
|
|
85
|
+
missing.push("start");
|
|
86
|
+
if (!actual.control.resume)
|
|
87
|
+
missing.push("resume");
|
|
88
|
+
if (!actual.control.sendTurn)
|
|
89
|
+
missing.push("send-turn");
|
|
90
|
+
if (!actual.control.interrupt)
|
|
91
|
+
missing.push("interrupt");
|
|
92
|
+
if (!actual.control.stop)
|
|
93
|
+
missing.push("stop");
|
|
94
|
+
if (actual.observation.sessionIdentity !== "exact")
|
|
95
|
+
missing.push("exact-session-identity");
|
|
96
|
+
if (actual.observation.promptAcceptance !== "exact")
|
|
97
|
+
missing.push("exact-prompt-acceptance");
|
|
98
|
+
if (actual.observation.turnLifecycle !== "exact")
|
|
99
|
+
missing.push("exact-turn-lifecycle");
|
|
100
|
+
return missing.length === 0
|
|
101
|
+
? Object.freeze({ admitted: true })
|
|
102
|
+
: Object.freeze({ admitted: false, missing: Object.freeze(missing) });
|
|
103
|
+
}
|
|
104
|
+
export class AgentDriverRegistry {
|
|
105
|
+
#drivers = new Map();
|
|
106
|
+
#driverIdsByAdapter = new Map();
|
|
107
|
+
register(input) {
|
|
108
|
+
const id = requireDriverId(input.id);
|
|
109
|
+
if (this.#drivers.has(id))
|
|
110
|
+
throw new Error(`Agent Driver is already registered: ${id}.`);
|
|
111
|
+
const adapterId = requireAdapterId(input.adapterId);
|
|
112
|
+
const existingDriverId = this.#driverIdsByAdapter.get(adapterId);
|
|
113
|
+
if (existingDriverId !== undefined) {
|
|
114
|
+
throw new Error(`Agent adapter ${adapterId} is already owned by Driver ${existingDriverId}.`);
|
|
115
|
+
}
|
|
116
|
+
if (input.protocolVersion !== 1) {
|
|
117
|
+
throw new Error(`Agent Driver protocol version is unsupported: ${String(input.protocolVersion)}.`);
|
|
118
|
+
}
|
|
119
|
+
if (input.runtime === null || typeof input.runtime !== "object") {
|
|
120
|
+
throw new Error("Agent Driver runtime must be an object.");
|
|
121
|
+
}
|
|
122
|
+
if (typeof input.runtime.nativeSessionId !== "function"
|
|
123
|
+
|| typeof input.runtime.nativeTurnId !== "function"
|
|
124
|
+
|| typeof input.runtime.mapHook !== "function"
|
|
125
|
+
|| typeof input.runtime.classifyHook !== "function"
|
|
126
|
+
|| (input.runtime.observer !== undefined
|
|
127
|
+
&& (typeof input.runtime.observer.source !== "function"
|
|
128
|
+
|| typeof input.runtime.observer.sample !== "function"))) {
|
|
129
|
+
throw new Error("Agent Driver runtime observation functions are invalid.");
|
|
130
|
+
}
|
|
131
|
+
const driver = Object.freeze({
|
|
132
|
+
id,
|
|
133
|
+
label: requireText(input.label, "Agent Driver label"),
|
|
134
|
+
protocolVersion: 1,
|
|
135
|
+
adapterId,
|
|
136
|
+
capabilities: validateAgentDriverCapabilities(input.capabilities),
|
|
137
|
+
runtime: Object.freeze({
|
|
138
|
+
nativeSessionId: input.runtime.nativeSessionId,
|
|
139
|
+
nativeTurnId: input.runtime.nativeTurnId,
|
|
140
|
+
mapHook: input.runtime.mapHook,
|
|
141
|
+
classifyHook: input.runtime.classifyHook,
|
|
142
|
+
...(input.runtime.observer === undefined
|
|
143
|
+
? {}
|
|
144
|
+
: { observer: Object.freeze({
|
|
145
|
+
source: input.runtime.observer.source,
|
|
146
|
+
sample: input.runtime.observer.sample
|
|
147
|
+
}) })
|
|
148
|
+
})
|
|
149
|
+
});
|
|
150
|
+
this.#drivers.set(id, driver);
|
|
151
|
+
this.#driverIdsByAdapter.set(adapterId, id);
|
|
152
|
+
return driver;
|
|
153
|
+
}
|
|
154
|
+
find(id) {
|
|
155
|
+
return this.#drivers.get(id) ?? null;
|
|
156
|
+
}
|
|
157
|
+
require(id) {
|
|
158
|
+
const driver = this.find(id);
|
|
159
|
+
if (driver === null)
|
|
160
|
+
throw new Error(`Agent Driver is not registered: ${id}.`);
|
|
161
|
+
return driver;
|
|
162
|
+
}
|
|
163
|
+
findByAdapterId(adapterId) {
|
|
164
|
+
const driverId = this.#driverIdsByAdapter.get(adapterId);
|
|
165
|
+
return driverId === undefined ? null : this.require(driverId);
|
|
166
|
+
}
|
|
167
|
+
requireByAdapterId(adapterId) {
|
|
168
|
+
const driver = this.findByAdapterId(adapterId);
|
|
169
|
+
if (driver === null) {
|
|
170
|
+
throw new Error(`Agent Driver is not registered for adapter: ${adapterId}.`);
|
|
171
|
+
}
|
|
172
|
+
return driver;
|
|
173
|
+
}
|
|
174
|
+
list() {
|
|
175
|
+
return [...this.#drivers.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export function requireDriverId(value) {
|
|
179
|
+
const id = requireText(value, "Agent Driver id");
|
|
180
|
+
if (!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/u.test(id)) {
|
|
181
|
+
throw new Error("Agent Driver id must be a lowercase namespaced identity.");
|
|
182
|
+
}
|
|
183
|
+
return id;
|
|
184
|
+
}
|
|
185
|
+
export function normalizeAgentDriverHookClassification(input) {
|
|
186
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
187
|
+
throw new Error("Agent Driver Hook classification must be an object.");
|
|
188
|
+
}
|
|
189
|
+
if (input.startupSession !== undefined
|
|
190
|
+
&& input.startupSession !== "preallocated"
|
|
191
|
+
&& input.startupSession !== "discovered") {
|
|
192
|
+
throw new Error("Agent Driver Hook startup Session classification is invalid.");
|
|
193
|
+
}
|
|
194
|
+
if (input.terminal !== undefined && typeof input.terminal !== "boolean") {
|
|
195
|
+
throw new Error("Agent Driver Hook terminal classification is invalid.");
|
|
196
|
+
}
|
|
197
|
+
return Object.freeze({
|
|
198
|
+
...(input.startupSession === undefined ? {} : { startupSession: input.startupSession }),
|
|
199
|
+
terminal: input.terminal ?? false
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function uniqueMembers(input, supported, label) {
|
|
203
|
+
if (!Array.isArray(input))
|
|
204
|
+
throw new Error(`${label}s must be an array.`);
|
|
205
|
+
const result = [];
|
|
206
|
+
for (const value of input) {
|
|
207
|
+
if (!supported.includes(value))
|
|
208
|
+
throw new Error(`${label} is invalid: ${String(value)}.`);
|
|
209
|
+
if (!result.includes(value))
|
|
210
|
+
result.push(value);
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
function requireText(value, label) {
|
|
215
|
+
if (typeof value !== "string" || value.includes("\0"))
|
|
216
|
+
throw new Error(`${label} is invalid.`);
|
|
217
|
+
const normalized = value.trim();
|
|
218
|
+
if (normalized.length === 0 || normalized.length > 1_024) {
|
|
219
|
+
throw new Error(`${label} is invalid.`);
|
|
220
|
+
}
|
|
221
|
+
return normalized;
|
|
222
|
+
}
|
|
223
|
+
function requireAdapterId(value) {
|
|
224
|
+
const id = requireText(value, "Agent adapter id");
|
|
225
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/u.test(id)) {
|
|
226
|
+
throw new Error("Agent adapter id must be a lowercase identity.");
|
|
227
|
+
}
|
|
228
|
+
return id;
|
|
229
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createRuntimeObservation } from "./runtimeObservation.js";
|
|
3
|
+
export function mapAgentDriverHook(input) {
|
|
4
|
+
requireMatchingDriver(input);
|
|
5
|
+
const mapped = input.driver.runtime.mapHook({
|
|
6
|
+
hookEventName: input.hookEventName,
|
|
7
|
+
payload: input.payload,
|
|
8
|
+
...(input.occurrenceId === undefined ? {} : { occurrenceId: input.occurrenceId })
|
|
9
|
+
});
|
|
10
|
+
const source = mapped.kind === "turn.accepted"
|
|
11
|
+
? input.driver.runtime.observer?.source({
|
|
12
|
+
hookEventName: input.hookEventName,
|
|
13
|
+
payload: input.payload,
|
|
14
|
+
...(input.occurrenceId === undefined ? {} : { occurrenceId: input.occurrenceId })
|
|
15
|
+
}) ?? null
|
|
16
|
+
: null;
|
|
17
|
+
return observation(input, source === null ? mapped : {
|
|
18
|
+
...mapped,
|
|
19
|
+
payload: Object.freeze({ ...mapped.payload, observerSource: source })
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function mapAgentDriverUsage(input, usage) {
|
|
23
|
+
requireMatchingDriver(input);
|
|
24
|
+
return observation(input, {
|
|
25
|
+
kind: "activity.observed",
|
|
26
|
+
payload: Object.freeze({ activity: "model", usage })
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function observation(input, mapped) {
|
|
30
|
+
return createRuntimeObservation({
|
|
31
|
+
schemaVersion: 1,
|
|
32
|
+
eventId: hookEventId(input, mapped),
|
|
33
|
+
kind: mapped.kind,
|
|
34
|
+
authority: "provider-structured",
|
|
35
|
+
receivedAt: input.receivedAt,
|
|
36
|
+
...(input.observedAt === undefined ? {} : { observedAt: input.observedAt }),
|
|
37
|
+
...(input.sequence === undefined ? {} : { sequence: input.sequence }),
|
|
38
|
+
...(input.ordinal === undefined ? {} : { ordinal: input.ordinal }),
|
|
39
|
+
fence: input.fence,
|
|
40
|
+
payload: mapped.payload
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function requireMatchingDriver(input) {
|
|
44
|
+
if (input.fence.driverId !== input.driver.id) {
|
|
45
|
+
throw new Error("Agent Driver Hook fence does not match the selected Driver.");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function hookEventId(input, mapped) {
|
|
49
|
+
const digest = createHash("sha256").update(JSON.stringify([
|
|
50
|
+
input.driver.id,
|
|
51
|
+
input.hookEventName,
|
|
52
|
+
input.fence,
|
|
53
|
+
mapped.kind,
|
|
54
|
+
mapped.payload
|
|
55
|
+
])).digest("hex");
|
|
56
|
+
return `runtime-observation-${digest}`;
|
|
57
|
+
}
|