@zq-silk/yui 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/ARCHITECTURE.md +27 -28
  2. package/README.md +57 -46
  3. package/dist/cli/commandCatalog.js +57 -17
  4. package/dist/cli/interactionPolicy.js +4 -10
  5. package/dist/cli/invocationRouter.js +2 -1
  6. package/dist/cli.js +106 -27
  7. package/dist/commands/taskCommands.js +458 -77
  8. package/dist/commands/taskCompletionGate.js +152 -0
  9. package/dist/context/runContextPack.js +19 -4
  10. package/dist/context/sessionBootstrapManifest.js +1 -1
  11. package/dist/controller/fileSchedulerStoreAdapter.js +389 -70
  12. package/dist/controller/resourceInventory.js +9 -5
  13. package/dist/controller/runtime.js +80 -7
  14. package/dist/controller/runtimeLaunchCoordinator.js +18 -78
  15. package/dist/controller/structuredProviderObservation.js +273 -0
  16. package/dist/executor/agentAdapter.js +40 -0
  17. package/dist/executor/agentExecutor.js +31 -7
  18. package/dist/executor/executorRegistry.js +11 -49
  19. package/dist/executor/fileRoleLaunchPlanner.js +115 -37
  20. package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
  21. package/dist/repository/gitWorkspace.js +7 -4
  22. package/dist/repository/taskBaseFreshness.js +4 -2
  23. package/dist/run/agentRun.js +4 -4
  24. package/dist/runtime/agentHost.js +767 -158
  25. package/dist/runtime/builtinAgentDrivers.js +1 -5
  26. package/dist/runtime/codexAppServerRuntime.js +67 -60
  27. package/dist/runtime/exactControlPlane.js +7 -2
  28. package/dist/runtime/index.js +6 -2
  29. package/dist/runtime/launchBroker.js +30 -8
  30. package/dist/runtime/providerAuthorityFence.js +24 -0
  31. package/dist/runtime/providerControl.js +63 -0
  32. package/dist/runtime/providerRecoveryDecision.js +55 -0
  33. package/dist/runtime/providerRuntimeIdentity.js +269 -19
  34. package/dist/runtime/runtimeBinding.js +20 -11
  35. package/dist/runtime/structuredProviderHost.js +476 -0
  36. package/dist/runtime/tmuxAdapters.js +143 -42
  37. package/dist/scheduler/activeRoleRunDelivery.js +206 -120
  38. package/dist/scheduler/leaderWakeupProcessor.js +141 -16
  39. package/dist/scheduler/wakeReason.js +1 -0
  40. package/dist/storage/migration/productionRegistry.js +111 -0
  41. package/dist/storage/sqliteStore.js +2 -0
  42. package/dist/storage/taskStore.js +3 -1
  43. package/dist/task/completionReadiness.js +43 -0
  44. package/dist/task/nextAction.js +6 -4
  45. package/dist/task/publicationReference.js +1 -0
  46. package/dist/tmux/tmuxManager.js +1 -1
  47. package/dist/workItem/workItem.js +12 -0
  48. package/dist/workspace/workItemChangeSetManager.js +2 -1
  49. package/i18n/README.zh-CN.md +11 -8
  50. package/package.json +1 -1
  51. package/skills/yui-leader/SKILL.md +8 -3
  52. package/skills/yui-runtime/SKILL.md +7 -2
@@ -0,0 +1,476 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { CodexAppServerRequestError, CodexAppServerRuntime, codexAppServerErrorIsMissing } from "./codexAppServerRuntime.js";
4
+ import { YUI_VERSION } from "../version.js";
5
+ const PROVIDER_MESSAGE_MAX_BYTES = 16 * 1024 * 1024;
6
+ const PROVIDER_ACCEPT_TIMEOUT_MS = 30_000;
7
+ export class ProviderDeliveryUnknownError extends Error {
8
+ attemptId;
9
+ name = "ProviderDeliveryUnknownError";
10
+ constructor(message, attemptId) {
11
+ super(message);
12
+ this.attemptId = attemptId;
13
+ }
14
+ }
15
+ export class ProviderTurnRejectedError extends Error {
16
+ attemptId;
17
+ name = "ProviderTurnRejectedError";
18
+ constructor(message, attemptId) {
19
+ super(message);
20
+ this.attemptId = attemptId;
21
+ }
22
+ }
23
+ export class ProviderConversationMissingError extends Error {
24
+ conversationId;
25
+ name = "ProviderConversationMissingError";
26
+ constructor(conversationId, message) {
27
+ super(message);
28
+ this.conversationId = conversationId;
29
+ }
30
+ }
31
+ export async function startStructuredProviderSession(payload, input = {}) {
32
+ const control = payload.providerControl;
33
+ if (control === undefined) {
34
+ throw new Error("Managed Agent Host launch requires Provider control metadata.");
35
+ }
36
+ const child = spawn(payload.command, [...payload.args], {
37
+ cwd: payload.cwd,
38
+ env: { ...payload.environment },
39
+ stdio: ["pipe", "pipe", "pipe"],
40
+ detached: true
41
+ });
42
+ const processInstanceId = randomUUID();
43
+ const mirror = input.mirrorOutput ?? defaultMirrorOutput;
44
+ child.stderr.setEncoding("utf8");
45
+ child.stderr.on("data", (chunk) => mirror("stderr", chunk));
46
+ const exit = childExit(child, processInstanceId);
47
+ try {
48
+ const session = control.adapterId === "codex"
49
+ ? await CodexStructuredProviderSession.open(child, exit, processInstanceId, payload, control, input.onTerminal, mirror)
50
+ : await ClaudeStructuredProviderSession.open(child, exit, processInstanceId, control, input.onTerminal, mirror);
51
+ return Object.freeze({ session });
52
+ }
53
+ catch (error) {
54
+ terminateProcessGroup(child, "SIGTERM");
55
+ throw error;
56
+ }
57
+ }
58
+ class JsonLineChannel {
59
+ child;
60
+ mirror;
61
+ #pending = new Map();
62
+ #listeners = new Set();
63
+ #buffer = "";
64
+ #nextId = 1;
65
+ #closedError;
66
+ constructor(child, mirror) {
67
+ this.child = child;
68
+ this.mirror = mirror;
69
+ child.stdout.setEncoding("utf8");
70
+ child.stdout.on("data", (chunk) => this.#receive(chunk));
71
+ child.once("error", (error) => this.#close(error));
72
+ child.once("close", (code, signal) => this.#close(new Error(`Provider process exited before replying (code=${code ?? "none"}, signal=${signal ?? "none"}).`)));
73
+ }
74
+ onMessage(listener) {
75
+ this.#listeners.add(listener);
76
+ return () => this.#listeners.delete(listener);
77
+ }
78
+ async request(method, params) {
79
+ if (this.#closedError !== undefined)
80
+ throw this.#closedError;
81
+ const id = String(this.#nextId++);
82
+ const response = new Promise((resolvePromise, reject) => {
83
+ const timer = setTimeout(() => {
84
+ this.#pending.delete(id);
85
+ reject(new Error(`Provider request timed out: ${method}.`));
86
+ }, PROVIDER_ACCEPT_TIMEOUT_MS);
87
+ this.#pending.set(id, { resolve: resolvePromise, reject, timer });
88
+ });
89
+ try {
90
+ await this.send({ id, method, params });
91
+ }
92
+ catch (error) {
93
+ const pending = this.#pending.get(id);
94
+ if (pending !== undefined) {
95
+ clearTimeout(pending.timer);
96
+ this.#pending.delete(id);
97
+ pending.reject(error);
98
+ }
99
+ }
100
+ return await response;
101
+ }
102
+ async notify(method, params) {
103
+ await this.send({ method, ...(params === undefined ? {} : { params }) });
104
+ }
105
+ async send(message) {
106
+ if (this.#closedError !== undefined)
107
+ throw this.#closedError;
108
+ const line = `${JSON.stringify(message)}\n`;
109
+ if (Buffer.byteLength(line, "utf8") > PROVIDER_MESSAGE_MAX_BYTES) {
110
+ throw new Error("Provider request exceeds its message bound.");
111
+ }
112
+ await new Promise((resolvePromise, reject) => {
113
+ this.child.stdin.write(line, "utf8", (error) => {
114
+ if (error === null || error === undefined)
115
+ resolvePromise();
116
+ else
117
+ reject(error);
118
+ });
119
+ });
120
+ }
121
+ #receive(chunk) {
122
+ this.mirror("stdout", chunk);
123
+ this.#buffer += chunk;
124
+ if (Buffer.byteLength(this.#buffer, "utf8") > PROVIDER_MESSAGE_MAX_BYTES) {
125
+ this.#close(new Error("Provider response line exceeds its message bound."));
126
+ terminateProcessGroup(this.child, "SIGTERM");
127
+ return;
128
+ }
129
+ for (;;) {
130
+ const newline = this.#buffer.indexOf("\n");
131
+ if (newline < 0)
132
+ return;
133
+ const line = this.#buffer.slice(0, newline).trim();
134
+ this.#buffer = this.#buffer.slice(newline + 1);
135
+ if (line.length === 0)
136
+ continue;
137
+ let message;
138
+ try {
139
+ const parsed = JSON.parse(line);
140
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
141
+ continue;
142
+ message = parsed;
143
+ }
144
+ catch {
145
+ continue;
146
+ }
147
+ const id = requestId(message.id);
148
+ const pending = id === undefined ? undefined : this.#pending.get(id);
149
+ if (pending !== undefined) {
150
+ clearTimeout(pending.timer);
151
+ this.#pending.delete(id);
152
+ const error = object(message.error);
153
+ if (error !== null) {
154
+ pending.reject(new CodexAppServerRequestError(typeof error.code === "number" || typeof error.code === "string"
155
+ ? error.code
156
+ : "UNKNOWN", typeof error.message === "string" ? error.message : "Provider request failed.", error.data));
157
+ }
158
+ else {
159
+ pending.resolve(object(message.result) ?? {});
160
+ }
161
+ continue;
162
+ }
163
+ for (const listener of this.#listeners)
164
+ listener(message);
165
+ }
166
+ }
167
+ #close(error) {
168
+ if (this.#closedError !== undefined)
169
+ return;
170
+ this.#closedError = error;
171
+ for (const pending of this.#pending.values()) {
172
+ clearTimeout(pending.timer);
173
+ pending.reject(error);
174
+ }
175
+ this.#pending.clear();
176
+ }
177
+ }
178
+ class CodexStructuredProviderSession {
179
+ child;
180
+ exit;
181
+ processInstanceId;
182
+ conversationId;
183
+ runtime;
184
+ adapterId = "codex";
185
+ #activeTurnId;
186
+ constructor(child, exit, processInstanceId, conversationId, runtime) {
187
+ this.child = child;
188
+ this.exit = exit;
189
+ this.processInstanceId = processInstanceId;
190
+ this.conversationId = conversationId;
191
+ this.runtime = runtime;
192
+ }
193
+ static async open(child, exit, processInstanceId, payload, control, onTerminal, mirror) {
194
+ const channel = new JsonLineChannel(child, mirror);
195
+ await channel.request("initialize", {
196
+ clientInfo: { name: "yui", title: "Yui", version: YUI_VERSION },
197
+ capabilities: {
198
+ experimentalApi: true,
199
+ requestAttestation: false
200
+ }
201
+ });
202
+ await channel.notify("initialized");
203
+ const runtime = new CodexAppServerRuntime(channel);
204
+ let conversationId;
205
+ let resumedActiveTurnId;
206
+ if (control.mode === "new") {
207
+ conversationId = (await runtime.openConversation({ cwd: payload.cwd })).conversationId;
208
+ }
209
+ else {
210
+ try {
211
+ const resumed = await runtime.resumeConversation(control.nativeSessionId);
212
+ conversationId = resumed.threadId;
213
+ resumedActiveTurnId = resumed.activeTurnId;
214
+ }
215
+ catch (error) {
216
+ if (codexAppServerErrorIsMissing(error)) {
217
+ throw new ProviderConversationMissingError(control.nativeSessionId, `Codex Conversation is exactly missing: ${control.nativeSessionId}.`);
218
+ }
219
+ throw error;
220
+ }
221
+ }
222
+ const session = new CodexStructuredProviderSession(child, exit, processInstanceId, conversationId, runtime);
223
+ session.#activeTurnId = resumedActiveTurnId;
224
+ channel.onMessage((message) => {
225
+ const method = typeof message.method === "string" ? message.method : "";
226
+ const params = object(message.params) ?? {};
227
+ if (optionalId(params.threadId) !== conversationId)
228
+ return;
229
+ if (method === "turn/started") {
230
+ const turnId = nestedId(params, "turn") ?? optionalId(params.turnId);
231
+ if (turnId !== undefined)
232
+ session.#activeTurnId = turnId;
233
+ return;
234
+ }
235
+ if (method !== "turn/completed")
236
+ return;
237
+ const turn = object(params.turn) ?? {};
238
+ const nativeTurnId = optionalId(turn.id) ?? optionalId(params.turnId);
239
+ if (nativeTurnId === undefined)
240
+ return;
241
+ session.#activeTurnId = undefined;
242
+ const status = turn.status === "failed"
243
+ ? "failed"
244
+ : turn.status === "interrupted" ? "cancelled" : "completed";
245
+ onTerminal?.({
246
+ conversationId,
247
+ nativeSessionId: conversationId,
248
+ nativeTurnId,
249
+ status,
250
+ observedAt: new Date().toISOString(),
251
+ ...(status !== "failed" ? {} : { error: providerErrorText(turn.error) })
252
+ });
253
+ });
254
+ return session;
255
+ }
256
+ get nativeSessionId() {
257
+ return this.conversationId;
258
+ }
259
+ get activeTurnId() {
260
+ return this.#activeTurnId;
261
+ }
262
+ async submitTurn(turn) {
263
+ if (this.#activeTurnId !== undefined) {
264
+ throw new ProviderTurnRejectedError(`Provider Conversation already has active Turn ${this.#activeTurnId}.`, turn.attemptId);
265
+ }
266
+ const acceptance = await this.runtime.submitTurn({
267
+ conversationId: this.conversationId,
268
+ attemptId: turn.attemptId,
269
+ text: turn.boundedText,
270
+ expectedNoActiveTurn: true
271
+ });
272
+ if (acceptance.status === "unknown") {
273
+ throw new ProviderDeliveryUnknownError(acceptance.reason, turn.attemptId);
274
+ }
275
+ if (acceptance.status === "not-accepted") {
276
+ throw new ProviderTurnRejectedError(acceptance.reason, turn.attemptId);
277
+ }
278
+ this.#activeTurnId = acceptance.turnId;
279
+ return Object.freeze({
280
+ attemptId: turn.attemptId,
281
+ conversationId: this.conversationId,
282
+ nativeSessionId: this.conversationId,
283
+ nativeTurnId: acceptance.turnId,
284
+ acceptedAt: new Date().toISOString()
285
+ });
286
+ }
287
+ waitForExit() {
288
+ return this.exit;
289
+ }
290
+ terminate(signal) {
291
+ terminateProcessGroup(this.child, signal);
292
+ }
293
+ }
294
+ class ClaudeStructuredProviderSession {
295
+ child;
296
+ exit;
297
+ processInstanceId;
298
+ conversationId;
299
+ channel;
300
+ adapterId = "claude";
301
+ #activeTurnId;
302
+ #waiter;
303
+ constructor(child, exit, processInstanceId, conversationId, channel) {
304
+ this.child = child;
305
+ this.exit = exit;
306
+ this.processInstanceId = processInstanceId;
307
+ this.conversationId = conversationId;
308
+ this.channel = channel;
309
+ }
310
+ static async open(child, exit, processInstanceId, control, onTerminal, mirror) {
311
+ const channel = new JsonLineChannel(child, mirror);
312
+ const nativeSessionId = control.nativeSessionId;
313
+ if (nativeSessionId === undefined) {
314
+ throw new Error("Managed Claude launch requires a preallocated native Session id.");
315
+ }
316
+ const session = new ClaudeStructuredProviderSession(child, exit, processInstanceId, nativeSessionId, channel);
317
+ channel.onMessage((message) => session.#receive(message, onTerminal));
318
+ exit.then(() => {
319
+ const waiter = session.#waiter;
320
+ if (waiter === undefined)
321
+ return;
322
+ clearTimeout(waiter.timer);
323
+ session.#waiter = undefined;
324
+ waiter.reject(new ProviderDeliveryUnknownError("Claude process exited before replaying the submitted user message.", waiter.attemptId));
325
+ }).catch(() => { });
326
+ return session;
327
+ }
328
+ get nativeSessionId() {
329
+ return this.conversationId;
330
+ }
331
+ get activeTurnId() {
332
+ return this.#activeTurnId;
333
+ }
334
+ async submitTurn(turn) {
335
+ if (this.#activeTurnId !== undefined || this.#waiter !== undefined) {
336
+ throw new ProviderTurnRejectedError("Provider Conversation already has an unsettled Turn.", turn.attemptId);
337
+ }
338
+ let deliveryTimer;
339
+ const receipt = new Promise((resolvePromise, reject) => {
340
+ deliveryTimer = setTimeout(() => {
341
+ if (this.#waiter?.attemptId === turn.attemptId)
342
+ this.#waiter = undefined;
343
+ reject(new ProviderDeliveryUnknownError("Claude did not replay the submitted user message before the acknowledgement deadline.", turn.attemptId));
344
+ }, PROVIDER_ACCEPT_TIMEOUT_MS);
345
+ this.#waiter = {
346
+ attemptId: turn.attemptId,
347
+ text: turn.boundedText,
348
+ resolve: resolvePromise,
349
+ reject,
350
+ timer: deliveryTimer
351
+ };
352
+ });
353
+ try {
354
+ await this.channel.send({
355
+ type: "user",
356
+ message: {
357
+ role: "user",
358
+ content: [{ type: "text", text: turn.boundedText }]
359
+ }
360
+ });
361
+ }
362
+ catch (error) {
363
+ if (deliveryTimer !== undefined)
364
+ clearTimeout(deliveryTimer);
365
+ this.#waiter = undefined;
366
+ throw new ProviderDeliveryUnknownError(`Claude input write did not produce an exact replay acknowledgement: ${error instanceof Error ? error.message : String(error)}`, turn.attemptId);
367
+ }
368
+ return await receipt;
369
+ }
370
+ waitForExit() {
371
+ return this.exit;
372
+ }
373
+ terminate(signal) {
374
+ terminateProcessGroup(this.child, signal);
375
+ }
376
+ #receive(message, onTerminal) {
377
+ if (message.type === "user") {
378
+ const waiter = this.#waiter;
379
+ if (waiter === undefined || claudeUserText(message) !== waiter.text)
380
+ return;
381
+ const observedSessionId = optionalId(message.session_id);
382
+ if (observedSessionId !== this.conversationId) {
383
+ clearTimeout(waiter.timer);
384
+ this.#waiter = undefined;
385
+ waiter.reject(new ProviderDeliveryUnknownError("Claude replayed input for a different native Session.", waiter.attemptId));
386
+ return;
387
+ }
388
+ const nativeTurnId = `claude-input:${waiter.attemptId}`;
389
+ this.#activeTurnId = nativeTurnId;
390
+ clearTimeout(waiter.timer);
391
+ this.#waiter = undefined;
392
+ waiter.resolve(Object.freeze({
393
+ attemptId: waiter.attemptId,
394
+ conversationId: this.conversationId,
395
+ nativeSessionId: this.conversationId,
396
+ nativeTurnId,
397
+ acceptedAt: new Date().toISOString()
398
+ }));
399
+ return;
400
+ }
401
+ if (message.type !== "result" || this.#activeTurnId === undefined
402
+ || optionalId(message.session_id) !== this.conversationId)
403
+ return;
404
+ const nativeTurnId = this.#activeTurnId;
405
+ this.#activeTurnId = undefined;
406
+ const failed = message.is_error === true || message.subtype === "error_during_execution";
407
+ onTerminal?.({
408
+ conversationId: this.conversationId,
409
+ nativeSessionId: this.conversationId,
410
+ nativeTurnId,
411
+ status: failed ? "failed" : "completed",
412
+ observedAt: new Date().toISOString(),
413
+ ...(typeof message.result === "string" && message.result.length > 0
414
+ ? failed ? { error: message.result } : { summary: message.result }
415
+ : {})
416
+ });
417
+ }
418
+ }
419
+ function childExit(child, processInstanceId) {
420
+ return new Promise((resolvePromise, reject) => {
421
+ child.once("error", reject);
422
+ child.once("close", (code, signal) => resolvePromise(Object.freeze({
423
+ code,
424
+ signal,
425
+ processInstanceId
426
+ })));
427
+ });
428
+ }
429
+ function terminateProcessGroup(child, signal) {
430
+ if (child.pid === undefined)
431
+ return;
432
+ // Managed Providers are spawned detached and therefore own a process group.
433
+ // Kill that exact group so a CLI helper cannot outlive the Agent Host. The
434
+ // direct-child fallback covers embedded runtimes that cannot create setsid.
435
+ try {
436
+ process.kill(-child.pid, signal);
437
+ }
438
+ catch (error) {
439
+ if (error.code !== "ESRCH")
440
+ throw error;
441
+ child.kill(signal);
442
+ }
443
+ }
444
+ function defaultMirrorOutput(stream, text) {
445
+ (stream === "stdout" ? process.stdout : process.stderr).write(text);
446
+ }
447
+ function requestId(value) {
448
+ return typeof value === "string" || typeof value === "number" ? String(value) : undefined;
449
+ }
450
+ function object(value) {
451
+ return value !== null && typeof value === "object" && !Array.isArray(value)
452
+ ? value
453
+ : null;
454
+ }
455
+ function optionalId(value) {
456
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
457
+ }
458
+ function nestedId(value, key) {
459
+ return optionalId(object(value[key])?.id);
460
+ }
461
+ function providerErrorText(value) {
462
+ const error = object(value);
463
+ if (typeof error?.message === "string" && error.message.length > 0)
464
+ return error.message;
465
+ return "Provider Turn failed.";
466
+ }
467
+ function claudeUserText(message) {
468
+ const content = object(message.message)?.content;
469
+ if (!Array.isArray(content))
470
+ return undefined;
471
+ const text = content.flatMap((entry) => {
472
+ const block = object(entry);
473
+ return block?.type === "text" && typeof block.text === "string" ? [block.text] : [];
474
+ });
475
+ return text.length === 1 ? text[0] : text.join("");
476
+ }