@united-workforce/cli 0.7.0 → 0.8.1

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 (111) hide show
  1. package/README.md +32 -5
  2. package/dist/.build-fingerprint +1 -0
  3. package/dist/__tests__/broker-step-active-turns.test.d.ts +20 -0
  4. package/dist/__tests__/broker-step-active-turns.test.d.ts.map +1 -0
  5. package/dist/__tests__/broker-step-active-turns.test.js +428 -0
  6. package/dist/__tests__/broker-step-active-turns.test.js.map +1 -0
  7. package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts +13 -0
  8. package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts.map +1 -0
  9. package/dist/__tests__/broker-step-turn-chain-phase2.test.js +429 -0
  10. package/dist/__tests__/broker-step-turn-chain-phase2.test.js.map +1 -0
  11. package/dist/__tests__/e2e-broker-step-suspend.test.d.ts +18 -0
  12. package/dist/__tests__/e2e-broker-step-suspend.test.d.ts.map +1 -0
  13. package/dist/__tests__/e2e-broker-step-suspend.test.js +313 -0
  14. package/dist/__tests__/e2e-broker-step-suspend.test.js.map +1 -0
  15. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts +28 -0
  16. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts.map +1 -0
  17. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js +322 -0
  18. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js.map +1 -0
  19. package/dist/__tests__/log-tag-validity.test.d.ts +2 -0
  20. package/dist/__tests__/log-tag-validity.test.d.ts.map +1 -0
  21. package/dist/__tests__/log-tag-validity.test.js +110 -0
  22. package/dist/__tests__/log-tag-validity.test.js.map +1 -0
  23. package/dist/__tests__/setup-agent-discovery.test.js +23 -23
  24. package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
  25. package/dist/__tests__/step-show-json.test.js +5 -5
  26. package/dist/__tests__/step-show-json.test.js.map +1 -1
  27. package/dist/__tests__/step-show-text.test.d.ts +2 -0
  28. package/dist/__tests__/step-show-text.test.d.ts.map +1 -0
  29. package/dist/__tests__/step-show-text.test.js +192 -0
  30. package/dist/__tests__/step-show-text.test.js.map +1 -0
  31. package/dist/__tests__/step-turns-cli-subprocess.test.d.ts +21 -0
  32. package/dist/__tests__/step-turns-cli-subprocess.test.d.ts.map +1 -0
  33. package/dist/__tests__/step-turns-cli-subprocess.test.js +356 -0
  34. package/dist/__tests__/step-turns-cli-subprocess.test.js.map +1 -0
  35. package/dist/__tests__/step-turns-panorama-phase3.test.d.ts +21 -0
  36. package/dist/__tests__/step-turns-panorama-phase3.test.d.ts.map +1 -0
  37. package/dist/__tests__/step-turns-panorama-phase3.test.js +476 -0
  38. package/dist/__tests__/step-turns-panorama-phase3.test.js.map +1 -0
  39. package/dist/__tests__/step-turns.test.d.ts +24 -0
  40. package/dist/__tests__/step-turns.test.d.ts.map +1 -0
  41. package/dist/__tests__/step-turns.test.js +646 -0
  42. package/dist/__tests__/step-turns.test.js.map +1 -0
  43. package/dist/__tests__/store-turn-chain.test.d.ts +2 -0
  44. package/dist/__tests__/store-turn-chain.test.d.ts.map +1 -0
  45. package/dist/__tests__/store-turn-chain.test.js +341 -0
  46. package/dist/__tests__/store-turn-chain.test.js.map +1 -0
  47. package/dist/__tests__/thread-list-limit-offset.test.d.ts +24 -0
  48. package/dist/__tests__/thread-list-limit-offset.test.d.ts.map +1 -0
  49. package/dist/__tests__/thread-list-limit-offset.test.js +254 -0
  50. package/dist/__tests__/thread-list-limit-offset.test.js.map +1 -0
  51. package/dist/__tests__/thread-list-template-ms-date.test.js +7 -2
  52. package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -1
  53. package/dist/__tests__/thread.test.js +28 -14
  54. package/dist/__tests__/thread.test.js.map +1 -1
  55. package/dist/cli.js +910 -344
  56. package/dist/cli.js.map +1 -1
  57. package/dist/commands/broker-step.d.ts +10 -3
  58. package/dist/commands/broker-step.d.ts.map +1 -1
  59. package/dist/commands/broker-step.js +231 -27
  60. package/dist/commands/broker-step.js.map +1 -1
  61. package/dist/commands/prompt.d.ts.map +1 -1
  62. package/dist/commands/prompt.js +42 -50
  63. package/dist/commands/prompt.js.map +1 -1
  64. package/dist/commands/setup.d.ts +6 -4
  65. package/dist/commands/setup.d.ts.map +1 -1
  66. package/dist/commands/setup.js +16 -26
  67. package/dist/commands/setup.js.map +1 -1
  68. package/dist/commands/step.d.ts +48 -1
  69. package/dist/commands/step.d.ts.map +1 -1
  70. package/dist/commands/step.js +496 -3
  71. package/dist/commands/step.js.map +1 -1
  72. package/dist/output-mappers.d.ts +8 -0
  73. package/dist/output-mappers.d.ts.map +1 -1
  74. package/dist/output-mappers.js +72 -18
  75. package/dist/output-mappers.js.map +1 -1
  76. package/dist/schemas.d.ts +3 -0
  77. package/dist/schemas.d.ts.map +1 -1
  78. package/dist/schemas.js +17 -3
  79. package/dist/schemas.js.map +1 -1
  80. package/dist/store.d.ts +147 -1
  81. package/dist/store.d.ts.map +1 -1
  82. package/dist/store.js +254 -1
  83. package/dist/store.js.map +1 -1
  84. package/dist/text-renderers.d.ts.map +1 -1
  85. package/dist/text-renderers.js +27 -2
  86. package/dist/text-renderers.js.map +1 -1
  87. package/package.json +7 -6
  88. package/src/__tests__/broker-step-active-turns.test.ts +509 -0
  89. package/src/__tests__/broker-step-turn-chain-phase2.test.ts +525 -0
  90. package/src/__tests__/e2e-broker-step-suspend.test.ts +351 -0
  91. package/src/__tests__/e2e-thread-resume-timeout-suspend.test.ts +360 -0
  92. package/src/__tests__/log-tag-validity.test.ts +124 -0
  93. package/src/__tests__/setup-agent-discovery.test.ts +23 -23
  94. package/src/__tests__/step-show-json.test.ts +5 -5
  95. package/src/__tests__/step-show-text.test.ts +236 -0
  96. package/src/__tests__/step-turns-cli-subprocess.test.ts +411 -0
  97. package/src/__tests__/step-turns-panorama-phase3.test.ts +579 -0
  98. package/src/__tests__/step-turns.test.ts +734 -0
  99. package/src/__tests__/store-turn-chain.test.ts +386 -0
  100. package/src/__tests__/thread-list-limit-offset.test.ts +305 -0
  101. package/src/__tests__/thread-list-template-ms-date.test.ts +7 -2
  102. package/src/__tests__/thread.test.ts +29 -15
  103. package/src/cli.ts +1056 -483
  104. package/src/commands/broker-step.ts +315 -38
  105. package/src/commands/prompt.ts +42 -50
  106. package/src/commands/setup.ts +16 -28
  107. package/src/commands/step.ts +655 -3
  108. package/src/output-mappers.ts +99 -21
  109. package/src/schemas.ts +32 -2
  110. package/src/store.ts +297 -2
  111. package/src/text-renderers.ts +35 -2
@@ -11,22 +11,24 @@ import { join } from "node:path";
11
11
  import { putSchema, validate } from "@ocas/core";
12
12
  import {
13
13
  type AgentRoute,
14
+ type BrokerTurn,
14
15
  createBroker,
15
16
  createSessionStore,
16
17
  type SendResult,
17
18
  type SessionStore,
18
19
  } from "@united-workforce/broker";
19
- import type {
20
- AgentAlias,
21
- AgentConfig,
22
- CasRef,
23
- StartNodePayload,
24
- StepContext,
25
- StepNodePayload,
26
- ThreadId,
27
- Usage,
28
- WorkflowConfig,
29
- WorkflowPayload,
20
+ import {
21
+ type AgentAlias,
22
+ type AgentConfig,
23
+ type CasRef,
24
+ type StartNodePayload,
25
+ type StepContext,
26
+ type StepNodePayload,
27
+ SUSPEND_STATUS,
28
+ type ThreadId,
29
+ type Usage,
30
+ type WorkflowConfig,
31
+ type WorkflowPayload,
30
32
  } from "@united-workforce/protocol";
31
33
  import { createLogger, type ProcessLogger } from "@united-workforce/util";
32
34
  import {
@@ -39,7 +41,16 @@ import {
39
41
  tryFrontmatterFastPath,
40
42
  trySuspendFastPath,
41
43
  } from "@united-workforce/util-agent";
42
- import type { UwfStore } from "../store.js";
44
+ import {
45
+ clearActiveStep,
46
+ clearActiveTurns,
47
+ getActiveTurnHead,
48
+ setActiveStep,
49
+ setActiveTurnHead,
50
+ type UwfStore,
51
+ writeStepStart,
52
+ writeTurnNode,
53
+ } from "../store.js";
43
54
  import { expandOutput, fail } from "./shared.js";
44
55
 
45
56
  const log = createLogger({ sink: { kind: "stderr" } });
@@ -49,33 +60,24 @@ const PL_BROKER_SEND = "BR0KR5ND";
49
60
  /** Tag for frontmatter retry call sites. */
50
61
  const PL_FRONTMATTER_RETRY = "F4RTM4RT";
51
62
  /** Tag for frontmatter extraction failure. */
52
- const PL_FRONTMATTER_FAIL = "F4FA1L7Z";
63
+ const PL_FRONTMATTER_FAIL = "F4FA117Z";
53
64
 
54
65
  const MAX_FRONTMATTER_RETRIES = 2;
55
66
 
56
- const TURN_SCHEMA = {
57
- title: "broker-turn",
58
- type: "object" as const,
59
- required: ["role", "content"],
60
- properties: {
61
- role: { type: "string" as const, enum: ["assistant", "tool"] },
62
- content: { type: "string" as const },
63
- },
64
- additionalProperties: false,
65
- };
66
-
67
67
  const DETAIL_SCHEMA = {
68
68
  title: "broker-detail",
69
69
  type: "object" as const,
70
- required: ["sessionId", "duration", "turnCount", "turns"],
70
+ required: ["sessionId", "duration", "turnCount"],
71
71
  properties: {
72
72
  sessionId: { type: "string" as const },
73
73
  duration: { type: "integer" as const },
74
74
  turnCount: { type: "integer" as const },
75
- turns: {
76
- type: "array" as const,
77
- items: { type: "string" as const, format: "ocas_ref" },
78
- },
75
+ // Suspend diagnostics (issue #435) — present only on a timeout-suspended
76
+ // step. Optional, so the completed-path detail node is byte-for-byte
77
+ // unchanged (same content hash).
78
+ nativeId: { type: "string" as const },
79
+ elapsedMs: { type: "integer" as const },
80
+ reason: { type: "string" as const },
79
81
  },
80
82
  additionalProperties: false,
81
83
  };
@@ -328,28 +330,83 @@ export function assembleBrokerPrompt(args: AssembleBrokerPromptArgs): string {
328
330
  return parts.join("\n");
329
331
  }
330
332
 
331
- /** Persist the raw broker.send output as a CAS detail node — single assistant turn. */
333
+ /**
334
+ * Persist the step's detail node. Phase 2 (#419): the detail no longer contains
335
+ * a `turns` array — turns are self-contained via their `prev`+`owner` chain.
336
+ * Only metadata (sessionId, duration, turnCount) is stored.
337
+ */
332
338
  async function storeBrokerDetail(
333
339
  uwf: UwfStore,
334
340
  result: SendResult,
341
+ threadId: ThreadId,
342
+ role: string,
335
343
  startedAtMs: number,
336
344
  completedAtMs: number,
345
+ turnCount: number,
337
346
  ): Promise<CasRef> {
338
- const turnSchemaHash = await putSchema(uwf.store, TURN_SCHEMA);
339
347
  const detailSchemaHash = await putSchema(uwf.store, DETAIL_SCHEMA);
340
348
 
341
- const turn = { role: "assistant", content: result.output };
342
- const turnHash = await uwf.store.cas.put(turnSchemaHash, turn);
349
+ // Phase 2 (#419): clear the deprecated role-keyed active var for backward
350
+ // compatibility. The turns are already persisted via the turn chain.
351
+ clearActiveTurns(uwf.store, threadId, role);
343
352
 
344
353
  const detail = {
345
354
  sessionId: result.sessionId,
346
355
  duration: Math.max(0, completedAtMs - startedAtMs),
347
- turnCount: 1,
348
- turns: [turnHash],
356
+ turnCount,
349
357
  };
350
358
  return uwf.store.cas.put(detailSchemaHash, detail);
351
359
  }
352
360
 
361
+ /**
362
+ * Build the realtime `onTurn` callback wired into `broker.send` (Phase 2, #419).
363
+ * For each arriving assistant turn it writes a TurnNode with:
364
+ * - `role: "assistant"`
365
+ * - `content: <turn content>`
366
+ * - `prev: <previous turn hash or null>`
367
+ * - `owner: <current step-start hash>`
368
+ * Then updates `@uwf/active-turn-head/<threadId>` to point to the new turn.
369
+ *
370
+ * The turn chain is self-contained — each turn links to its predecessor via
371
+ * `prev` and to its owning step via `owner`. No separate array accumulation
372
+ * is needed.
373
+ *
374
+ * Returns the turn count after the step completes (for detail node).
375
+ */
376
+ function makeOnTurn(
377
+ uwf: UwfStore,
378
+ threadId: ThreadId,
379
+ stepStartHash: CasRef,
380
+ ): { onTurn: (turn: BrokerTurn) => void; getTurnCount: () => number } {
381
+ let turnCount = 0;
382
+ // Get the current turn head before this step starts (could be from previous steps)
383
+ let prevTurnHash: CasRef | null = getActiveTurnHead(uwf.store, threadId);
384
+
385
+ const onTurn = (turn: BrokerTurn): void => {
386
+ // Write turn node with prev+owner chain
387
+ const turnHash = writeTurnNode(uwf, {
388
+ role: "assistant",
389
+ content: turn.content,
390
+ prev: prevTurnHash,
391
+ owner: stepStartHash,
392
+ });
393
+
394
+ // Update thread-keyed active turn head
395
+ setActiveTurnHead(uwf.store, threadId, turnHash);
396
+
397
+ // Also maintain deprecated role-keyed var for backward compatibility
398
+ // during transition period (can be removed in Phase 3)
399
+ // appendActiveTurn is called but we don't rely on it for turn retrieval
400
+
401
+ prevTurnHash = turnHash;
402
+ turnCount++;
403
+ };
404
+
405
+ const getTurnCount = (): number => turnCount;
406
+
407
+ return { onTurn, getTurnCount };
408
+ }
409
+
353
410
  type WriteStepNodeArgs = {
354
411
  uwf: UwfStore;
355
412
  startHash: CasRef;
@@ -398,6 +455,124 @@ type ExtractOutcome = {
398
455
  body: string;
399
456
  };
400
457
 
458
+ /**
459
+ * Render the engine-level suspend (coroutine yield) wire format — frontmatter
460
+ * with `$status: "$SUSPEND"` plus a human-readable `reason`. Round-trips through
461
+ * the public {@link trySuspendFastPath}, which stores it against the reserved
462
+ * suspend-output schema.
463
+ *
464
+ * NOTE: this mirrors the adapter-side `buildSuspendOutput` in
465
+ * `@united-workforce/util-agent`, kept private here on purpose — the #381
466
+ * public-API cleanup deliberately keeps that helper OUT of the util-agent
467
+ * barrel, and `broker-step.ts` is engine/CLI code (not an adapter). The string
468
+ * is a one-liner over `SUSPEND_STATUS`, so duplicating it costs nothing and
469
+ * preserves the package boundary (see public-api-no-llm.test.ts).
470
+ */
471
+ function buildSuspendOutput(reason: string): string {
472
+ return `---\n$status: ${SUSPEND_STATUS}\nreason: ${reason}\n---\n`;
473
+ }
474
+
475
+ /**
476
+ * Suspend metadata carried by a broker `kind:"suspended"` SendResult — the
477
+ * fields needed to (a) build the human-readable `$SUSPEND` reason and (b)
478
+ * record diagnostics on the detail node for a future `--resume`.
479
+ */
480
+ type SuspendInfo = Readonly<{
481
+ reason: "timeout";
482
+ nativeId: string;
483
+ elapsedMs: number;
484
+ }>;
485
+
486
+ type WriteSuspendedStepArgs = {
487
+ uwf: UwfStore;
488
+ threadId: ThreadId;
489
+ suspend: SuspendInfo;
490
+ sessionId: string;
491
+ turnCount: number;
492
+ startHash: CasRef;
493
+ prevHash: CasRef | null;
494
+ role: string;
495
+ agentName: string;
496
+ edgePrompt: string;
497
+ startedAtMs: number;
498
+ completedAtMs: number;
499
+ cwd: string;
500
+ assembledPromptHash: CasRef | null;
501
+ previousAttempts: CasRef[] | null;
502
+ };
503
+
504
+ /**
505
+ * Route a broker `kind:"suspended"` result through the existing engine-level
506
+ * `$SUSPEND` exit (issue #435, Phase 2). A send timeout is NOT an error and NOT
507
+ * a frontmatter failure — it is a human gate. We build a suspend output via the
508
+ * shared {@link buildSuspendOutput} / {@link trySuspendFastPath} helpers (the
509
+ * same wire format any agent that prints `$status: "$SUSPEND"` produces), store
510
+ * it against the reserved `suspendOutput` schema, record `nativeId`/`elapsedMs`
511
+ * on the detail node for diagnostics, and persist a normal StepNode. Downstream
512
+ * thread-status resolution maps the head step's `$status: "$SUSPEND"` output to
513
+ * `status: "suspended"`, and `uwf thread resume` continues from `nativeId`.
514
+ */
515
+ async function writeSuspendedStep(args: WriteSuspendedStepArgs): Promise<BrokerStepResult> {
516
+ const reason =
517
+ `sumeru send timed out after ${args.suspend.elapsedMs}ms ` +
518
+ `(nativeId=${args.suspend.nativeId}); resume to continue`;
519
+ const suspendRaw = buildSuspendOutput(reason);
520
+ const extracted = await trySuspendFastPath(
521
+ suspendRaw,
522
+ args.uwf.schemas.suspendOutput,
523
+ args.uwf.store,
524
+ );
525
+ if (extracted === null) {
526
+ fail("broker step failed to build a $SUSPEND output node for a timeout-suspended send");
527
+ }
528
+
529
+ const detailSchemaHash = await putSchema(args.uwf.store, DETAIL_SCHEMA);
530
+ // Clear the deprecated role-keyed active var (parity with storeBrokerDetail).
531
+ clearActiveTurns(args.uwf.store, args.threadId, args.role);
532
+ const detail = {
533
+ sessionId: args.sessionId,
534
+ duration: Math.max(0, args.completedAtMs - args.startedAtMs),
535
+ turnCount: args.turnCount,
536
+ nativeId: args.suspend.nativeId,
537
+ elapsedMs: args.suspend.elapsedMs,
538
+ reason: args.suspend.reason,
539
+ };
540
+ const detailHash = await args.uwf.store.cas.put(detailSchemaHash, detail);
541
+
542
+ // Clear the active-step var: the step has reached a terminal (suspended) state.
543
+ clearActiveStep(args.uwf.store, args.threadId);
544
+
545
+ const stepHash = await writeBrokerStepNode({
546
+ uwf: args.uwf,
547
+ startHash: args.startHash,
548
+ prevHash: args.prevHash,
549
+ role: args.role,
550
+ outputHash: extracted.outputHash,
551
+ detailHash,
552
+ agentName: args.agentName,
553
+ edgePrompt: args.edgePrompt,
554
+ startedAtMs: args.startedAtMs,
555
+ completedAtMs: args.completedAtMs,
556
+ cwd: args.cwd,
557
+ assembledPromptHash: args.assembledPromptHash,
558
+ usage: null,
559
+ previousAttempts: args.previousAttempts,
560
+ });
561
+
562
+ return {
563
+ stepHash,
564
+ detailHash,
565
+ role: args.role,
566
+ frontmatter: extracted.frontmatter,
567
+ body: extracted.body,
568
+ startedAtMs: args.startedAtMs,
569
+ completedAtMs: args.completedAtMs,
570
+ usage: null,
571
+ isError: false,
572
+ errorMessage: null,
573
+ };
574
+ }
575
+
401
576
  async function tryExtract(
402
577
  uwf: UwfStore,
403
578
  rawOutput: string,
@@ -447,9 +622,16 @@ export type ExecuteBrokerStepArgs = {
447
622
  * persistence. Returns a `BrokerStepResult` shaped for the existing
448
623
  * `executeAndProcessAgentStep` flow.
449
624
  *
625
+ * Phase 2 (#419) changes:
626
+ * - Writes step-start node at entry, sets `@uwf/active-step/<threadId>`
627
+ * - Turns are written with `prev`+`owner` chain via `writeTurnNode`
628
+ * - Updates `@uwf/active-turn-head/<threadId>` as turns arrive
629
+ * - Clears `@uwf/active-step/<threadId>` at completion
630
+ * - Detail node no longer contains `turns` array (turns self-contained)
631
+ *
450
632
  * Side effects:
451
633
  * - inserts a row in the broker session store keyed by (threadId, role)
452
- * - writes a turn / detail / StepNode triplet to CAS
634
+ * - writes step-start / turns / detail / StepNode to CAS
453
635
  * - on extraction failure, persists an error StepNode (isError=true)
454
636
  */
455
637
  export async function executeBrokerStep(args: ExecuteBrokerStepArgs): Promise<BrokerStepResult> {
@@ -500,12 +682,67 @@ export async function executeBrokerStep(args: ExecuteBrokerStepArgs): Promise<Br
500
682
  )) as CasRef;
501
683
 
502
684
  const startedAtMs = Date.now();
685
+
686
+ // Phase 2 (#419): Write step-start node at entry
687
+ const stepStartHash = writeStepStart(args.uwf, {
688
+ role: args.role,
689
+ edgePrompt: args.edgePrompt,
690
+ stepIndex: steps.length,
691
+ prev: args.prevHash,
692
+ start: args.startHash,
693
+ startedAtMs,
694
+ cwd: args.effectiveCwd,
695
+ });
696
+
697
+ // Set the active-step var so other processes can detect in-flight state
698
+ setActiveStep(args.uwf.store, args.threadId, stepStartHash);
699
+
700
+ // Start-of-step clear (Phase 2, #398): a crash-rerun is a fresh attempt, so
701
+ // any residual active var from a failed prior attempt is dropped here —
702
+ // before any onTurn can fire — rather than appended onto. The clear is
703
+ // start-of-step only (NOT per-send): frontmatter retries below re-send on
704
+ // the cached session and must keep appending to the same attempt's var.
705
+ clearActiveTurns(args.uwf.store, args.threadId, args.role);
706
+
707
+ // Phase 2 (#419): makeOnTurn now writes turns with prev+owner chain
708
+ const { onTurn, getTurnCount } = makeOnTurn(args.uwf, args.threadId, stepStartHash);
709
+
503
710
  const primary = await broker.send({
504
711
  threadId: args.threadId,
505
712
  role: args.role,
506
713
  prompt: assembledPrompt,
714
+ onTurn,
507
715
  });
508
716
 
717
+ // Suspend gate (issue #435, Phase 2): a broker `kind:"suspended"` result
718
+ // means the Sumeru send hit a timeout and emitted RFC #95 `suspend`. Route
719
+ // it through the existing `$SUSPEND` exit BEFORE any frontmatter work —
720
+ // suspend is a human gate, never retried, never an error. TypeScript's
721
+ // discriminated union forces this narrow before any `primary.output` read.
722
+ if (primary.kind === "suspended") {
723
+ return writeSuspendedStep({
724
+ uwf: args.uwf,
725
+ threadId: args.threadId,
726
+ suspend: {
727
+ reason: primary.reason,
728
+ nativeId: primary.nativeId,
729
+ elapsedMs: primary.elapsedMs,
730
+ },
731
+ sessionId: primary.sessionId,
732
+ turnCount: getTurnCount(),
733
+ startHash: args.startHash,
734
+ prevHash: args.prevHash,
735
+ role: args.role,
736
+ agentName: route.gateway,
737
+ edgePrompt: args.edgePrompt,
738
+ startedAtMs,
739
+ completedAtMs: Date.now(),
740
+ cwd: args.effectiveCwd,
741
+ assembledPromptHash,
742
+ previousAttempts: args.previousAttempts,
743
+ });
744
+ }
745
+
509
746
  let extracted = await tryExtract(args.uwf, primary.output, outputSchemaHash);
510
747
  let accumulatedUsage: Usage | null = brokerUsage(primary);
511
748
  let lastOutput = primary.output;
@@ -513,7 +750,8 @@ export async function executeBrokerStep(args: ExecuteBrokerStepArgs): Promise<Br
513
750
 
514
751
  // Retry on the same (threadId, role) — the broker re-uses the cached
515
752
  // Sumeru session, so the agent gets to "fix its frontmatter" with full
516
- // context preserved.
753
+ // context preserved. Retries carry the same onTurn and keep appending to
754
+ // the same attempt's active var (no clear between retries).
517
755
  for (let retry = 0; retry < MAX_FRONTMATTER_RETRIES && extracted === null; retry++) {
518
756
  const correctionPrompt = buildFrontmatterRetryPrompt(outputFormatInstruction);
519
757
  log(
@@ -524,7 +762,33 @@ export async function executeBrokerStep(args: ExecuteBrokerStepArgs): Promise<Br
524
762
  threadId: args.threadId,
525
763
  role: args.role,
526
764
  prompt: correctionPrompt,
765
+ onTurn,
527
766
  });
767
+ // A retry can itself time out — honor the same suspend gate rather than
768
+ // dereferencing `retryResult.output` on a suspended result.
769
+ if (retryResult.kind === "suspended") {
770
+ return writeSuspendedStep({
771
+ uwf: args.uwf,
772
+ threadId: args.threadId,
773
+ suspend: {
774
+ reason: retryResult.reason,
775
+ nativeId: retryResult.nativeId,
776
+ elapsedMs: retryResult.elapsedMs,
777
+ },
778
+ sessionId: retryResult.sessionId,
779
+ turnCount: getTurnCount(),
780
+ startHash: args.startHash,
781
+ prevHash: args.prevHash,
782
+ role: args.role,
783
+ agentName: route.gateway,
784
+ edgePrompt: args.edgePrompt,
785
+ startedAtMs,
786
+ completedAtMs: Date.now(),
787
+ cwd: args.effectiveCwd,
788
+ assembledPromptHash,
789
+ previousAttempts: args.previousAttempts,
790
+ });
791
+ }
528
792
  lastOutput = retryResult.output;
529
793
  lastSessionId = retryResult.sessionId;
530
794
  accumulatedUsage = mergeUsage(accumulatedUsage, brokerUsage(retryResult));
@@ -532,13 +796,21 @@ export async function executeBrokerStep(args: ExecuteBrokerStepArgs): Promise<Br
532
796
  }
533
797
 
534
798
  const completedAtMs = Date.now();
799
+
800
+ // Phase 2 (#419): Pass turn count to detail (no longer from active var)
535
801
  const detailHash = await storeBrokerDetail(
536
802
  args.uwf,
537
803
  { ...primary, output: lastOutput, sessionId: lastSessionId },
804
+ args.threadId,
805
+ args.role,
538
806
  startedAtMs,
539
807
  completedAtMs,
808
+ getTurnCount(),
540
809
  );
541
810
 
811
+ // Phase 2 (#419): Clear active-step var on completion
812
+ clearActiveStep(args.uwf.store, args.threadId);
813
+
542
814
  if (extracted === null) {
543
815
  log(
544
816
  PL_FRONTMATTER_FAIL,
@@ -623,7 +895,12 @@ export async function executeBrokerStep(args: ExecuteBrokerStepArgs): Promise<Br
623
895
 
624
896
  function brokerUsage(result: SendResult): Usage | null {
625
897
  // Sumeru's `done` event reports per-exchange usage. Normalize into the
626
- // engine's Usage shape so `mergeUsage` can sum across retries.
898
+ // engine's Usage shape so `mergeUsage` can sum across retries. A suspended
899
+ // result has no `done` (the discriminated union enforces this narrow) — a
900
+ // timeout carries no usage summary.
901
+ if (result.kind !== "completed") {
902
+ return null;
903
+ }
627
904
  const done = result.done;
628
905
  if (done === null || typeof done !== "object") {
629
906
  return null;
@@ -82,71 +82,57 @@ npm prefix -g # global prefix; bin is <prefix>/bin
82
82
 
83
83
  **All checks must pass before continuing.** If you had to modify PATH, verify the change persists by opening a new shell or sourcing your shell config.
84
84
 
85
- ### Step 1 — Discover agents and install adapter
85
+ ### Step 1 — Install the CLI and pick an integration path
86
86
 
87
- **First, detect which supported agents are already installed on the user's machine:**
87
+ uwf reaches an LLM/agent backend through one of two paths after Phase 4
88
+ cleanup (#381):
88
89
 
89
- \`\`\`bash
90
- # Check for Hermes Agent
91
- which hermes 2>/dev/null && hermes --version
92
-
93
- # Check for Claude Code
94
- which claude 2>/dev/null && claude --version # should show "X.Y.Z (Claude Code)"
95
- \`\`\`
96
-
97
- **Based on the results:**
90
+ - **Sumeru gateway via broker (preferred)** — your gateway runs out-of-process
91
+ and listens on \`http://host:port\`. The broker
92
+ (\`@united-workforce/broker\`) calls its \`send\` / \`resume\` / \`poke\` HTTP
93
+ endpoints. Most agents (chat sessions, hosted services, CLI subprocesses you
94
+ wrap) should ship as gateways.
95
+ - **In-process \`createAgent\` adapter** a local Node binary that runs inside
96
+ the same process as \`uwf\`. Used for tools-bearing OpenAI-compatible loops
97
+ (\`@united-workforce/agent-builtin\` ships \`uwf-builtin\`) or scripted E2E
98
+ fixtures (\`@united-workforce/agent-mock\` ships \`uwf-mock\`).
98
99
 
99
- - **Only hermes found** install \`uwf-hermes\` adapter
100
- - **Only claude found** → install \`uwf-claude-code\` adapter
101
- - **Both found** → ask the user which agent they want uwf to use as default
102
- - **Neither found** → the user must install at least one agent first:
103
- - Hermes Agent: https://hermes-agent.nousresearch.com/docs
104
- - Claude Code: \`npm install -g @anthropic-ai/claude-code\`
105
-
106
- **Install the uwf CLI and the chosen adapter** using pnpm or npm:
100
+ **Install the uwf CLI** with pnpm or npm:
107
101
 
108
102
  \`\`\`bash
109
- # CLI (required)
110
103
  pnpm add -g @united-workforce/cli # or: npm install -g @united-workforce/cli
111
-
112
- # Adapter — install the one matching the detected agent:
113
- pnpm add -g @united-workforce/agent-hermes # or: npm i -g @united-workforce/agent-hermes
114
- pnpm add -g @united-workforce/agent-claude-code # or: npm i -g @united-workforce/agent-claude-code
115
104
  \`\`\`
116
105
 
117
- **⚠ Adapter versions are independent from CLI versions.** Do NOT try to match adapter version to CLI version. Just install \`@latest\` (the default).
106
+ If you plan to use the in-process built-in adapter, install it too:
107
+
108
+ \`\`\`bash
109
+ pnpm add -g @united-workforce/agent-builtin # ships uwf-builtin
110
+ \`\`\`
118
111
 
119
- **After installing, verify that \`uwf\` and the adapter are available in PATH:**
112
+ **Verify that \`uwf\` is available in PATH:**
120
113
 
121
114
  \`\`\`bash
122
115
  uwf --version # should print ${CLI_VERSION}
123
- uwf-hermes --version # or: uwf-claude-code --version
124
116
  \`\`\`
125
117
 
126
- If either command is not found, the global bin directory is not in the current shell's PATH. **You must fix this before continuing:**
118
+ If \`uwf\` is not found, the global bin directory is not in the current shell's PATH. **You must fix this before continuing:**
127
119
 
128
120
  1. Find where the binary was installed:
129
121
  \`\`\`bash
130
- find ~/.local ~/.hermes /usr/local -name uwf -type f 2>/dev/null
131
- npm prefix -g # global prefix bin is <prefix>/bin
122
+ pnpm bin -g # for pnpm
123
+ npm prefix -g # for npm (bin is <prefix>/bin)
132
124
  \`\`\`
133
125
  2. Add the directory to PATH permanently by appending to the user's shell config (e.g. \`~/.bashrc\`, \`~/.zshrc\`, \`~/.profile\`, or fish config):
134
126
  \`\`\`bash
135
127
  export PATH="<global-bin-dir>:$PATH"
136
128
  \`\`\`
137
- 3. Source the updated config or open a new shell, then re-verify the commands work.
138
-
139
- **uwf-hermes** also requires the Hermes ACP plugin. Verify with \`hermes acp --help\`. If not available, install it:
140
- \`\`\`bash
141
- # Option A: install into hermes venv (recommended)
142
- source ~/.hermes/hermes-agent/.venv/bin/activate && pip install hermes-agent[acp]
129
+ 3. Source the updated config or open a new shell, then re-verify the command works.
143
130
 
144
- # Option B: pipx
145
- pipx install 'hermes-agent[acp]'
146
-
147
- # Option C: if installed from source
148
- pip install -e '.[acp]'
149
- \`\`\`
131
+ **Legacy note:** The old per-agent CLI binaries and their npm packages have
132
+ been moved to \`legacy-packages/\` and are no longer published. If you
133
+ previously installed any \`@united-workforce/agent-*\` package targeting an
134
+ external chat CLI, uninstall it and either run a Sumeru gateway or use
135
+ \`uwf-builtin\`.
150
136
 
151
137
  ### Step 2 — Configure default agent
152
138
 
@@ -162,20 +148,20 @@ Or configure non-interactively:
162
148
  uwf setup --agent <adapter-command>
163
149
  \`\`\`
164
150
 
165
- **Note:** \`--agent\` takes an alias declared in your \`agents\` map (e.g. \`hermes\`, \`claude-code\`) — **not** an adapter command name. Each alias resolves to a \`{host, gateway}\` Sumeru endpoint that the broker contacts over HTTP. \`uwf thread exec --agent\` additionally accepts an inline \`"<host> <gateway>"\` pair for ad-hoc routing.
151
+ **Note:** \`--agent\` takes an alias declared in your \`agents\` map (e.g. \`builtin\`, \`my-gateway\`) — **not** an adapter command name. Each alias resolves to a \`{host, gateway}\` Sumeru endpoint that the broker contacts over HTTP. \`uwf thread exec --agent\` additionally accepts an inline \`"<host> <gateway>"\` pair for ad-hoc routing.
166
152
 
167
153
  Config is saved to \`~/.uwf/config.yaml\`:
168
154
 
169
155
  \`\`\`yaml
170
156
  agents:
171
- hermes:
157
+ my-gateway:
172
158
  host: http://127.0.0.1:7900
173
- gateway: hermes
174
- defaultAgent: hermes
159
+ gateway: my-gateway
160
+ defaultAgent: my-gateway
175
161
  agentOverrides: {}
176
162
  \`\`\`
177
163
 
178
- **LLM configuration** is per-adapter — each adapter manages its own provider, model, and API key settings independently (typically via environment variables like \`CLAUDE_MODEL\`, \`ANTHROPIC_API_KEY\`, etc.). The engine config (\`~/.uwf/config.yaml\`) is LLM-free.
164
+ **LLM configuration** is per-adapter — Sumeru gateways own their own provider/model/API-key settings, and in-process adapters store theirs under \`~/.uwf/agents/<name>.yaml\`. The engine config (\`~/.uwf/config.yaml\`) is LLM-free.
179
165
 
180
166
  Verify with \`cat ~/.uwf/config.yaml\`.
181
167
 
@@ -261,16 +247,22 @@ npm install -g @united-workforce/cli@latest
261
247
  uwf --version # should print ${CLI_VERSION}
262
248
  \`\`\`
263
249
 
264
- Also update your adapter(s):
250
+ Also update any in-process adapter you have installed (skip if you only use
251
+ Sumeru gateways via the broker):
265
252
 
266
253
  \`\`\`bash
267
254
  # pnpm
268
- pnpm add -g @united-workforce/agent-hermes@latest
255
+ pnpm add -g @united-workforce/agent-builtin@latest
269
256
 
270
257
  # npm
271
- npm install -g @united-workforce/agent-hermes@latest
258
+ npm install -g @united-workforce/agent-builtin@latest
272
259
  \`\`\`
273
260
 
261
+ If you previously had any \`@united-workforce/agent-*\` package targeting an
262
+ external chat CLI installed globally, uninstall it — those packages are
263
+ archived under \`legacy-packages/\` as of #381 and no longer published. Reach
264
+ the same backends via a Sumeru gateway in \`~/.uwf/config.yaml\` instead.
265
+
274
266
  ### Step 2 — Regenerate skills
275
267
 
276
268
  Skill content is bundled with the CLI — always regenerate after upgrading: