@united-workforce/cli 0.6.1 → 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 (167) hide show
  1. package/README.md +120 -5
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__tests__/agent-resolution-llm-free.test.js +9 -2
  4. package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -1
  5. package/dist/__tests__/broker-prompt.test.d.ts +10 -0
  6. package/dist/__tests__/broker-prompt.test.d.ts.map +1 -0
  7. package/dist/__tests__/broker-prompt.test.js +129 -0
  8. package/dist/__tests__/broker-prompt.test.js.map +1 -0
  9. package/dist/__tests__/broker-step-active-turns.test.d.ts +20 -0
  10. package/dist/__tests__/broker-step-active-turns.test.d.ts.map +1 -0
  11. package/dist/__tests__/broker-step-active-turns.test.js +428 -0
  12. package/dist/__tests__/broker-step-active-turns.test.js.map +1 -0
  13. package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts +13 -0
  14. package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts.map +1 -0
  15. package/dist/__tests__/broker-step-turn-chain-phase2.test.js +429 -0
  16. package/dist/__tests__/broker-step-turn-chain-phase2.test.js.map +1 -0
  17. package/dist/__tests__/config.test.js +33 -37
  18. package/dist/__tests__/config.test.js.map +1 -1
  19. package/dist/__tests__/e2e-broker-step-suspend.test.d.ts +18 -0
  20. package/dist/__tests__/e2e-broker-step-suspend.test.d.ts.map +1 -0
  21. package/dist/__tests__/e2e-broker-step-suspend.test.js +313 -0
  22. package/dist/__tests__/e2e-broker-step-suspend.test.js.map +1 -0
  23. package/dist/__tests__/e2e-broker-step.test.d.ts +13 -0
  24. package/dist/__tests__/e2e-broker-step.test.d.ts.map +1 -0
  25. package/dist/__tests__/e2e-broker-step.test.js +278 -0
  26. package/dist/__tests__/e2e-broker-step.test.js.map +1 -0
  27. package/dist/__tests__/e2e-mock-agent.test.js +1 -1
  28. package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
  29. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts +28 -0
  30. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts.map +1 -0
  31. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js +322 -0
  32. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js.map +1 -0
  33. package/dist/__tests__/log-tag-validity.test.d.ts +2 -0
  34. package/dist/__tests__/log-tag-validity.test.d.ts.map +1 -0
  35. package/dist/__tests__/log-tag-validity.test.js +110 -0
  36. package/dist/__tests__/log-tag-validity.test.js.map +1 -0
  37. package/dist/__tests__/setup-agent-discovery.test.js +35 -23
  38. package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
  39. package/dist/__tests__/setup-no-llm.test.js +5 -2
  40. package/dist/__tests__/setup-no-llm.test.js.map +1 -1
  41. package/dist/__tests__/step-ask.test.js +9 -6
  42. package/dist/__tests__/step-ask.test.js.map +1 -1
  43. package/dist/__tests__/step-show-json.test.js +5 -5
  44. package/dist/__tests__/step-show-json.test.js.map +1 -1
  45. package/dist/__tests__/step-show-text.test.d.ts +2 -0
  46. package/dist/__tests__/step-show-text.test.d.ts.map +1 -0
  47. package/dist/__tests__/step-show-text.test.js +192 -0
  48. package/dist/__tests__/step-show-text.test.js.map +1 -0
  49. package/dist/__tests__/step-turns-cli-subprocess.test.d.ts +21 -0
  50. package/dist/__tests__/step-turns-cli-subprocess.test.d.ts.map +1 -0
  51. package/dist/__tests__/step-turns-cli-subprocess.test.js +356 -0
  52. package/dist/__tests__/step-turns-cli-subprocess.test.js.map +1 -0
  53. package/dist/__tests__/step-turns-panorama-phase3.test.d.ts +21 -0
  54. package/dist/__tests__/step-turns-panorama-phase3.test.d.ts.map +1 -0
  55. package/dist/__tests__/step-turns-panorama-phase3.test.js +476 -0
  56. package/dist/__tests__/step-turns-panorama-phase3.test.js.map +1 -0
  57. package/dist/__tests__/step-turns.test.d.ts +24 -0
  58. package/dist/__tests__/step-turns.test.d.ts.map +1 -0
  59. package/dist/__tests__/step-turns.test.js +646 -0
  60. package/dist/__tests__/step-turns.test.js.map +1 -0
  61. package/dist/__tests__/store-turn-chain.test.d.ts +2 -0
  62. package/dist/__tests__/store-turn-chain.test.d.ts.map +1 -0
  63. package/dist/__tests__/store-turn-chain.test.js +341 -0
  64. package/dist/__tests__/store-turn-chain.test.js.map +1 -0
  65. package/dist/__tests__/thread-agent-failure-suspended.test.js +3 -3
  66. package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -1
  67. package/dist/__tests__/thread-list-limit-offset.test.d.ts +24 -0
  68. package/dist/__tests__/thread-list-limit-offset.test.d.ts.map +1 -0
  69. package/dist/__tests__/thread-list-limit-offset.test.js +254 -0
  70. package/dist/__tests__/thread-list-limit-offset.test.js.map +1 -0
  71. package/dist/__tests__/thread-list-template-ms-date.test.js +7 -2
  72. package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -1
  73. package/dist/__tests__/thread-poke.test.js +6 -6
  74. package/dist/__tests__/thread-poke.test.js.map +1 -1
  75. package/dist/__tests__/thread-resume.test.js +2 -2
  76. package/dist/__tests__/thread-resume.test.js.map +1 -1
  77. package/dist/__tests__/thread-suspend-step.test.js +1 -1
  78. package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
  79. package/dist/__tests__/thread.test.js +28 -14
  80. package/dist/__tests__/thread.test.js.map +1 -1
  81. package/dist/cli.js +910 -344
  82. package/dist/cli.js.map +1 -1
  83. package/dist/commands/broker-step.d.ts +117 -0
  84. package/dist/commands/broker-step.d.ts.map +1 -0
  85. package/dist/commands/broker-step.js +654 -0
  86. package/dist/commands/broker-step.js.map +1 -0
  87. package/dist/commands/config.d.ts.map +1 -1
  88. package/dist/commands/config.js +2 -23
  89. package/dist/commands/config.js.map +1 -1
  90. package/dist/commands/prompt.d.ts.map +1 -1
  91. package/dist/commands/prompt.js +43 -51
  92. package/dist/commands/prompt.js.map +1 -1
  93. package/dist/commands/setup.d.ts +6 -4
  94. package/dist/commands/setup.d.ts.map +1 -1
  95. package/dist/commands/setup.js +24 -27
  96. package/dist/commands/setup.js.map +1 -1
  97. package/dist/commands/step.d.ts +54 -6
  98. package/dist/commands/step.d.ts.map +1 -1
  99. package/dist/commands/step.js +484 -134
  100. package/dist/commands/step.js.map +1 -1
  101. package/dist/commands/thread.d.ts +4 -0
  102. package/dist/commands/thread.d.ts.map +1 -1
  103. package/dist/commands/thread.js +77 -151
  104. package/dist/commands/thread.js.map +1 -1
  105. package/dist/output-mappers.d.ts +8 -0
  106. package/dist/output-mappers.d.ts.map +1 -1
  107. package/dist/output-mappers.js +72 -18
  108. package/dist/output-mappers.js.map +1 -1
  109. package/dist/schemas.d.ts +3 -0
  110. package/dist/schemas.d.ts.map +1 -1
  111. package/dist/schemas.js +17 -3
  112. package/dist/schemas.js.map +1 -1
  113. package/dist/store.d.ts +147 -1
  114. package/dist/store.d.ts.map +1 -1
  115. package/dist/store.js +254 -1
  116. package/dist/store.js.map +1 -1
  117. package/dist/text-renderers.d.ts.map +1 -1
  118. package/dist/text-renderers.js +27 -2
  119. package/dist/text-renderers.js.map +1 -1
  120. package/package.json +7 -5
  121. package/src/__tests__/agent-resolution-llm-free.test.ts +14 -2
  122. package/src/__tests__/broker-prompt.test.ts +142 -0
  123. package/src/__tests__/broker-step-active-turns.test.ts +509 -0
  124. package/src/__tests__/broker-step-turn-chain-phase2.test.ts +525 -0
  125. package/src/__tests__/config.test.ts +35 -39
  126. package/src/__tests__/e2e-broker-step-suspend.test.ts +351 -0
  127. package/src/__tests__/e2e-broker-step.test.ts +320 -0
  128. package/src/__tests__/e2e-mock-agent.test.ts +1 -1
  129. package/src/__tests__/e2e-thread-resume-timeout-suspend.test.ts +360 -0
  130. package/src/__tests__/log-tag-validity.test.ts +124 -0
  131. package/src/__tests__/setup-agent-discovery.test.ts +35 -23
  132. package/src/__tests__/setup-no-llm.test.ts +5 -2
  133. package/src/__tests__/step-ask.test.ts +9 -6
  134. package/src/__tests__/step-show-json.test.ts +5 -5
  135. package/src/__tests__/step-show-text.test.ts +236 -0
  136. package/src/__tests__/step-turns-cli-subprocess.test.ts +411 -0
  137. package/src/__tests__/step-turns-panorama-phase3.test.ts +579 -0
  138. package/src/__tests__/step-turns.test.ts +734 -0
  139. package/src/__tests__/store-turn-chain.test.ts +386 -0
  140. package/src/__tests__/thread-agent-failure-suspended.test.ts +3 -3
  141. package/src/__tests__/thread-list-limit-offset.test.ts +305 -0
  142. package/src/__tests__/thread-list-template-ms-date.test.ts +7 -2
  143. package/src/__tests__/thread-poke.test.ts +6 -6
  144. package/src/__tests__/thread-resume.test.ts +2 -2
  145. package/src/__tests__/thread-suspend-step.test.ts +1 -1
  146. package/src/__tests__/thread.test.ts +29 -15
  147. package/src/cli.ts +1056 -483
  148. package/src/commands/broker-step.ts +913 -0
  149. package/src/commands/config.ts +2 -24
  150. package/src/commands/prompt.ts +43 -51
  151. package/src/commands/setup.ts +25 -29
  152. package/src/commands/step.ts +645 -176
  153. package/src/commands/thread.ts +87 -192
  154. package/src/output-mappers.ts +99 -21
  155. package/src/schemas.ts +32 -2
  156. package/src/store.ts +297 -2
  157. package/src/text-renderers.ts +35 -2
  158. package/dist/__tests__/adapter-json-roundtrip.test.d.ts +0 -2
  159. package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +0 -1
  160. package/dist/__tests__/adapter-json-roundtrip.test.js +0 -160
  161. package/dist/__tests__/adapter-json-roundtrip.test.js.map +0 -1
  162. package/dist/__tests__/spawn-agent-json.test.d.ts +0 -2
  163. package/dist/__tests__/spawn-agent-json.test.d.ts.map +0 -1
  164. package/dist/__tests__/spawn-agent-json.test.js +0 -79
  165. package/dist/__tests__/spawn-agent-json.test.js.map +0 -1
  166. package/src/__tests__/adapter-json-roundtrip.test.ts +0 -193
  167. package/src/__tests__/spawn-agent-json.test.ts +0 -100
package/src/store.ts CHANGED
@@ -4,9 +4,16 @@ import { access, mkdir, readdir, readFile, rename } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join, resolve as resolvePath } from "node:path";
6
6
 
7
- import { bootstrap, type Hash, type Store, type VarStore } from "@ocas/core";
7
+ import { bootstrap, type Hash, putSchema, type Store, type VarStore } from "@ocas/core";
8
8
  import { createFsStore, createSqliteVarStore } from "@ocas/fs";
9
- import type { CasRef, ThreadId, ThreadIndexEntry, ThreadsIndex } from "@united-workforce/protocol";
9
+ import type {
10
+ CasRef,
11
+ StepStartPayload,
12
+ ThreadId,
13
+ ThreadIndexEntry,
14
+ ThreadsIndex,
15
+ TurnNodePayload,
16
+ } from "@united-workforce/protocol";
10
17
  import { parseThreadsIndex } from "@united-workforce/protocol";
11
18
  import { parse } from "yaml";
12
19
 
@@ -20,6 +27,232 @@ export const REGISTRY_VAR_PREFIX = "@uwf/registry/";
20
27
  /** Variable name prefix for active thread entries (`@uwf/thread/<thread-id>`). */
21
28
  export const THREAD_VAR_PREFIX = "@uwf/thread/";
22
29
 
30
+ /**
31
+ * Variable name prefix for the in-flight turn list of a running step
32
+ * (`@uwf/active-turns/<thread-id>/<role>`). Phase 2 of the realtime-turns RFC
33
+ * (#398): broker-step appends each assistant turn hash here as it arrives, so
34
+ * an independent process can observe a step's progress mid-flight. The var is a
35
+ * mutable head pointer at an immutable CAS array node; it is cleared at the
36
+ * start of each step and deleted once the turns are solidified into the
37
+ * step's immutable `detail.turns`.
38
+ *
39
+ * @deprecated Phase 2 (#419) — role-keyed vars replaced by thread-keyed:
40
+ * - `@uwf/active-step/<threadId>` — current in-flight step-start hash
41
+ * - `@uwf/active-turn-head/<threadId>` — head of the turn chain
42
+ */
43
+ export const ACTIVE_TURNS_VAR_PREFIX = "@uwf/active-turns/";
44
+
45
+ /**
46
+ * Schema for the active-turns list node: a bare ordered array of turn-hash
47
+ * `ocas_ref`s. Because an ocas variable is keyed by `(name, schema)` where the
48
+ * schema is the pointed-at node's `type`, this schema must be stable so that
49
+ * re-pointing the var (append), removing it (clear/solidify), and listing it by
50
+ * exact name all address the same variable. The hash is content-addressed and
51
+ * therefore identical across processes.
52
+ *
53
+ * @deprecated Phase 2 (#419) — role-keyed vars replaced by thread-keyed vars.
54
+ */
55
+ export const ACTIVE_TURNS_LIST_SCHEMA = {
56
+ title: "uwf-active-turns",
57
+ type: "array" as const,
58
+ items: { type: "string" as const, format: "ocas_ref" },
59
+ };
60
+
61
+ /**
62
+ * Build the active-turns variable name for a `(threadId, role)` pair.
63
+ *
64
+ * @deprecated Phase 2 (#419) — role-keyed vars replaced by thread-keyed vars.
65
+ */
66
+ export function activeTurnsVarName(threadId: ThreadId, role: string): string {
67
+ return `${ACTIVE_TURNS_VAR_PREFIX}${threadId}/${role}`;
68
+ }
69
+
70
+ /**
71
+ * Register (idempotently) and return the CAS schema hash for the active-turns
72
+ * list node. Used both as the array node's type and — implicitly — as the
73
+ * variable's schema key.
74
+ *
75
+ * @deprecated Phase 2 (#419) — role-keyed vars replaced by thread-keyed vars.
76
+ */
77
+ export function activeTurnsListSchemaHash(store: Store): Hash {
78
+ return putSchema(store, ACTIVE_TURNS_LIST_SCHEMA);
79
+ }
80
+
81
+ /**
82
+ * Read the ordered turn-hash list currently pointed at by
83
+ * `@uwf/active-turns/<threadId>/<role>`. Returns `[]` when the var does not
84
+ * exist (no turns appended yet, or already solidified/cleared).
85
+ *
86
+ * @deprecated Phase 2 (#419) — use `turnsOfStep()` with thread-keyed vars instead.
87
+ */
88
+ export function readActiveTurns(store: Store, threadId: ThreadId, role: string): CasRef[] {
89
+ const name = activeTurnsVarName(threadId, role);
90
+ const vars = store.var.list({ exactName: name });
91
+ const v = vars[0];
92
+ if (v === undefined) {
93
+ return [];
94
+ }
95
+ const node = store.cas.get(v.value as CasRef);
96
+ if (node === null || !Array.isArray(node.payload)) {
97
+ return [];
98
+ }
99
+ return node.payload as CasRef[];
100
+ }
101
+
102
+ /**
103
+ * Discover every in-flight role for a thread by scanning the
104
+ * `@uwf/active-turns/<threadId>/` var namespace. Returns each role whose active
105
+ * var currently holds a **non-empty** ordered turn-hash list, in var-creation
106
+ * order (a chronological proxy for "which in-flight step started first"). Used by
107
+ * `step turns` to append the running step(s) to the whole-chain panorama — the
108
+ * in-flight step has no settled StepNode hash, so it is found via its
109
+ * `(threadId, role)` active var rather than the chain.
110
+ *
111
+ * @deprecated Phase 2 (#419) — use `getActiveStep()` with thread-keyed vars instead.
112
+ */
113
+ export function readActiveTurnRoles(
114
+ store: Store,
115
+ threadId: ThreadId,
116
+ ): { role: string; turns: CasRef[] }[] {
117
+ const prefix = `${ACTIVE_TURNS_VAR_PREFIX}${threadId}/`;
118
+ const vars = store.var.list({ namePrefix: prefix });
119
+ const sorted = [...vars].sort((a, b) => a.created - b.created);
120
+ const result: { role: string; turns: CasRef[] }[] = [];
121
+ for (const v of sorted) {
122
+ const role = v.name.slice(prefix.length);
123
+ if (role === "" || role.includes("/")) {
124
+ continue;
125
+ }
126
+ const node = store.cas.get(v.value as CasRef);
127
+ if (node === null || !Array.isArray(node.payload)) {
128
+ continue;
129
+ }
130
+ const turns = node.payload as CasRef[];
131
+ if (turns.length === 0) {
132
+ continue;
133
+ }
134
+ result.push({ role, turns });
135
+ }
136
+ return result;
137
+ }
138
+
139
+ /**
140
+ * Append a turn hash to `@uwf/active-turns/<threadId>/<role>` (read-modify-write
141
+ * on the array node, then re-point the var). The var is a single mutable
142
+ * pointer re-pointed on each append — not one var per turn. Returns the full
143
+ * updated list.
144
+ *
145
+ * @deprecated Phase 2 (#419) — turns now written directly with prev+owner chain.
146
+ */
147
+ export function appendActiveTurn(
148
+ store: Store,
149
+ threadId: ThreadId,
150
+ role: string,
151
+ turnHash: CasRef,
152
+ ): CasRef[] {
153
+ const name = activeTurnsVarName(threadId, role);
154
+ const current = readActiveTurns(store, threadId, role);
155
+ const next = [...current, turnHash];
156
+ const schemaHash = activeTurnsListSchemaHash(store);
157
+ const listHash = store.cas.put(schemaHash, next) as CasRef;
158
+ store.var.set(name, listHash);
159
+ return next;
160
+ }
161
+
162
+ /**
163
+ * Clear (delete) the `@uwf/active-turns/<threadId>/<role>` pointer. Removing a
164
+ * missing var is a no-op, so this is safe to call at the start of a clean run.
165
+ * Targets the exact `(threadId, role)` var only — concurrent active vars for
166
+ * other roles/threads are untouched.
167
+ *
168
+ * @deprecated Phase 2 (#419) — turns now written directly with prev+owner chain.
169
+ */
170
+ export function clearActiveTurns(store: Store, threadId: ThreadId, role: string): void {
171
+ const name = activeTurnsVarName(threadId, role);
172
+ store.var.remove(name);
173
+ }
174
+
175
+ // ── Thread-keyed Active Vars (Phase 2 #419) ──────────────────────────
176
+
177
+ /**
178
+ * Variable name prefix for in-flight step (`@uwf/active-step/<thread-id>`).
179
+ * Phase 2 (#419): points to the current step-start hash while a step is in-flight.
180
+ * Cleared when the step completes (step-complete written).
181
+ */
182
+ export const ACTIVE_STEP_VAR_PREFIX = "@uwf/active-step/";
183
+
184
+ /**
185
+ * Variable name prefix for the turn chain head (`@uwf/active-turn-head/<thread-id>`).
186
+ * Phase 2 (#419): points to the most recent turn hash. Updated as each turn arrives.
187
+ * Remains after step completion (turns are immutable).
188
+ */
189
+ export const ACTIVE_TURN_HEAD_VAR_PREFIX = "@uwf/active-turn-head/";
190
+
191
+ /** Build the active-step variable name for a thread. */
192
+ export function activeStepVarName(threadId: ThreadId): string {
193
+ return `${ACTIVE_STEP_VAR_PREFIX}${threadId}`;
194
+ }
195
+
196
+ /** Build the active-turn-head variable name for a thread. */
197
+ export function activeTurnHeadVarName(threadId: ThreadId): string {
198
+ return `${ACTIVE_TURN_HEAD_VAR_PREFIX}${threadId}`;
199
+ }
200
+
201
+ /**
202
+ * Get the current in-flight step-start hash for a thread, or null if no step
203
+ * is in-flight. Phase 2 (#419): thread-keyed, not role-keyed.
204
+ */
205
+ export function getActiveStep(store: Store, threadId: ThreadId): CasRef | null {
206
+ const name = activeStepVarName(threadId);
207
+ const vars = store.var.list({ exactName: name });
208
+ const v = vars[0];
209
+ if (v === undefined) {
210
+ return null;
211
+ }
212
+ return v.value as CasRef;
213
+ }
214
+
215
+ /**
216
+ * Set the in-flight step-start hash for a thread. Called at step start.
217
+ * Phase 2 (#419): thread-keyed, not role-keyed.
218
+ */
219
+ export function setActiveStep(store: Store, threadId: ThreadId, stepStartHash: CasRef): void {
220
+ const name = activeStepVarName(threadId);
221
+ store.var.set(name, stepStartHash);
222
+ }
223
+
224
+ /**
225
+ * Clear the in-flight step-start for a thread. Called at step completion.
226
+ * Phase 2 (#419): thread-keyed, not role-keyed.
227
+ */
228
+ export function clearActiveStep(store: Store, threadId: ThreadId): void {
229
+ const name = activeStepVarName(threadId);
230
+ store.var.remove(name);
231
+ }
232
+
233
+ /**
234
+ * Get the current turn chain head hash for a thread, or null if no turns exist.
235
+ * Phase 2 (#419): thread-keyed, not role-keyed.
236
+ */
237
+ export function getActiveTurnHead(store: Store, threadId: ThreadId): CasRef | null {
238
+ const name = activeTurnHeadVarName(threadId);
239
+ const vars = store.var.list({ exactName: name });
240
+ const v = vars[0];
241
+ if (v === undefined) {
242
+ return null;
243
+ }
244
+ return v.value as CasRef;
245
+ }
246
+
247
+ /**
248
+ * Set the turn chain head hash for a thread. Called after each turn is written.
249
+ * Phase 2 (#419): thread-keyed, not role-keyed.
250
+ */
251
+ export function setActiveTurnHead(store: Store, threadId: ThreadId, turnHash: CasRef): void {
252
+ const name = activeTurnHeadVarName(threadId);
253
+ store.var.set(name, turnHash);
254
+ }
255
+
23
256
  /** A workflow entry discovered from the project-local .workflows/ (primary) or .workflow/ (legacy) directory. */
24
257
  export type ProjectWorkflowEntry = {
25
258
  /** Workflow name (from YAML `name` field, equals filename stem). */
@@ -567,3 +800,65 @@ export function migrateHistoryVarsToThreadVars(varStore: VarStore): void {
567
800
  varStore.remove(v.name);
568
801
  }
569
802
  }
803
+
804
+ // ── Turn Chain Functions (Phase 1) ─────────────────────────────────
805
+
806
+ /**
807
+ * Write a step-start node to CAS. Returns the CAS hash.
808
+ */
809
+ export function writeStepStart(uwfStore: UwfStore, payload: StepStartPayload): CasRef {
810
+ const hash = uwfStore.store.cas.put(uwfStore.schemas.stepStart, payload);
811
+ return hash as CasRef;
812
+ }
813
+
814
+ /**
815
+ * Write a turn node to CAS. Returns the CAS hash.
816
+ */
817
+ export function writeTurnNode(uwfStore: UwfStore, payload: TurnNodePayload): CasRef {
818
+ const hash = uwfStore.store.cas.put(uwfStore.schemas.turnNode, payload);
819
+ return hash as CasRef;
820
+ }
821
+
822
+ /**
823
+ * Walk the turn chain from head back to the first turn via `prev` pointers.
824
+ * Returns turns in chronological order (oldest first).
825
+ */
826
+ export function walkTurnChain(uwfStore: UwfStore, headHash: CasRef): CasRef[] {
827
+ const chain: CasRef[] = [];
828
+ let currentHash: CasRef | null = headHash;
829
+
830
+ while (currentHash !== null) {
831
+ chain.push(currentHash);
832
+ const node = uwfStore.store.cas.get(currentHash);
833
+ if (node === null) {
834
+ break;
835
+ }
836
+ const payload = node.payload as TurnNodePayload | { prev?: CasRef | null };
837
+ currentHash = payload.prev ?? null;
838
+ }
839
+
840
+ // Reverse to get chronological order (oldest first)
841
+ return chain.reverse();
842
+ }
843
+
844
+ /**
845
+ * Get turns belonging to a specific step-start by filtering the turn chain
846
+ * via the `owner` field. Returns turns in chronological order (oldest first).
847
+ */
848
+ export function turnsOfStep(uwfStore: UwfStore, headHash: CasRef, stepStartHash: CasRef): CasRef[] {
849
+ const allTurns = walkTurnChain(uwfStore, headHash);
850
+ const result: CasRef[] = [];
851
+
852
+ for (const turnHash of allTurns) {
853
+ const node = uwfStore.store.cas.get(turnHash);
854
+ if (node === null) {
855
+ continue;
856
+ }
857
+ const payload = node.payload as TurnNodePayload | { owner?: CasRef | null };
858
+ if (payload.owner === stepStartHash) {
859
+ result.push(turnHash);
860
+ }
861
+ }
862
+
863
+ return result;
864
+ }
@@ -81,6 +81,13 @@ type ThreadStopPayload = {
81
81
  stopped: boolean;
82
82
  };
83
83
 
84
+ type StepDetailUsage = {
85
+ turns: number;
86
+ inputTokens: number;
87
+ outputTokens: number;
88
+ duration: number;
89
+ };
90
+
84
91
  type StepDetailPayload = {
85
92
  hash: string;
86
93
  role: string;
@@ -89,8 +96,10 @@ type StepDetailPayload = {
89
96
  startedAtMs: number | null;
90
97
  completedAtMs: number | null;
91
98
  durationMs: number | null;
99
+ usage: StepDetailUsage | null;
92
100
  frontmatter: Record<string, unknown>;
93
101
  turns: Array<{ role: string; content: string; timestamp: number | null }>;
102
+ detail: Record<string, unknown> | null;
94
103
  };
95
104
 
96
105
  function asObject(data: unknown): Record<string, unknown> {
@@ -223,15 +232,39 @@ export function renderStepList(data: unknown): string {
223
232
  return lines.join("\n");
224
233
  }
225
234
 
235
+ function renderUsageLine(usage: unknown): string | null {
236
+ if (usage === null || usage === undefined || typeof usage !== "object") return null;
237
+ const u = usage as StepDetailUsage;
238
+ const inputTokens = typeof u.inputTokens === "number" ? u.inputTokens : 0;
239
+ const outputTokens = typeof u.outputTokens === "number" ? u.outputTokens : 0;
240
+ const turns = typeof u.turns === "number" ? u.turns : 0;
241
+ return `Usage ${inputTokens} in / ${outputTokens} out / ${turns} turns`;
242
+ }
243
+
226
244
  export function renderStepShow(data: unknown): string {
227
245
  const p = asObject(data) as Partial<StepDetailPayload>;
228
- return [
246
+ const lines: string[] = [
229
247
  `Step ${asString(p.hash)}`,
230
248
  `Role ${asString(p.role)}`,
231
249
  `Agent ${asString(p.agent)}`,
232
250
  `Status ${asString(p.status)}`,
233
251
  `Duration ${formatDuration(p.durationMs)}`,
234
- ].join("\n");
252
+ ];
253
+ const usageLine = renderUsageLine(p.usage);
254
+ if (usageLine !== null) lines.push(usageLine);
255
+ const turnsArr = Array.isArray(p.turns) ? p.turns : [];
256
+ if (turnsArr.length > 0) {
257
+ lines.push(`Turns ${turnsArr.length}`);
258
+ lines.push("");
259
+ lines.push("--- Content ---");
260
+ for (const turn of turnsArr) {
261
+ const t = asObject(turn);
262
+ const role = asString(t.role, "assistant");
263
+ const content = typeof t.content === "string" ? t.content : "";
264
+ lines.push(`[${role}] ${content}`);
265
+ }
266
+ }
267
+ return lines.join("\n");
235
268
  }
236
269
 
237
270
  export function renderThreadCancel(data: unknown): string {
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=adapter-json-roundtrip.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter-json-roundtrip.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adapter-json-roundtrip.test.ts"],"names":[],"mappings":""}
@@ -1,160 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { putSchema } from "@ocas/core";
7
- import { openStore } from "@ocas/fs";
8
- import { afterEach, beforeEach, describe, expect, test } from "vitest";
9
- import { registerUwfSchemas } from "../schemas.js";
10
- import { seedThreads } from "./thread-test-helpers.js";
11
- // ── schemas ──────────────────────────────────────────────────────────────────
12
- const OUTPUT_SCHEMA = {
13
- type: "object",
14
- properties: {
15
- $status: { type: "string", enum: ["done", "failed"] },
16
- result: { type: "string" },
17
- },
18
- required: ["$status"],
19
- additionalProperties: false,
20
- };
21
- // ── fixture ──────────────────────────────────────────────────────────────────
22
- let tmpDir;
23
- let savedOcasHome;
24
- beforeEach(async () => {
25
- savedOcasHome = process.env.OCAS_HOME;
26
- tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-roundtrip-test-"));
27
- });
28
- afterEach(async () => {
29
- if (savedOcasHome === undefined) {
30
- delete process.env.OCAS_HOME;
31
- }
32
- else {
33
- process.env.OCAS_HOME = savedOcasHome;
34
- }
35
- await rm(tmpDir, { recursive: true, force: true });
36
- });
37
- describe("C1: adapter JSON round-trip integration", () => {
38
- test("mock agent outputs JSON, CLI parses it and updates thread head in CAS", async () => {
39
- // 1. Set up CAS store with workflow, start node, and output schema
40
- const casDir = join(tmpDir, "cas");
41
- await mkdir(casDir, { recursive: true });
42
- const store = await openStore(casDir);
43
- const schemas = await registerUwfSchemas(store);
44
- const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA);
45
- const workflowHash = await store.cas.put(schemas.workflow, {
46
- name: "test-roundtrip",
47
- description: "roundtrip integration test",
48
- roles: {
49
- worker: {
50
- description: "Worker role",
51
- goal: "Do work",
52
- capabilities: [],
53
- procedure: "work",
54
- output: "result",
55
- frontmatter: outputSchemaHash,
56
- },
57
- },
58
- graph: {
59
- $START: {
60
- new: { role: "worker", prompt: "Do the work", location: null },
61
- resume: { role: "worker", prompt: "Resume the work", location: null },
62
- },
63
- worker: { done: { role: "$END", prompt: "completed", location: null } },
64
- },
65
- });
66
- const startHash = await store.cas.put(schemas.startNode, {
67
- workflow: workflowHash,
68
- prompt: "Test round-trip task",
69
- });
70
- process.env.OCAS_HOME = casDir;
71
- const threadId = "01ROUNDTRIPTEST0000000000";
72
- await seedThreads(tmpDir, { [threadId]: startHash });
73
- // 2. Pre-create CAS nodes that the mock agent would produce
74
- const outputHash = await store.cas.put(outputSchemaHash, {
75
- $status: "done",
76
- result: "test-ok",
77
- });
78
- // Use text schema for detail (simple placeholder)
79
- const detailHash = await store.cas.put(schemas.text, "mock detail");
80
- const startedAtMs = 1716600000000;
81
- const completedAtMs = 1716600001500;
82
- const stepHash = await store.cas.put(schemas.stepNode, {
83
- start: startHash,
84
- prev: null,
85
- role: "worker",
86
- output: outputHash,
87
- detail: detailHash,
88
- agent: "uwf-mock",
89
- edgePrompt: "Do the work",
90
- startedAtMs,
91
- completedAtMs,
92
- cwd: tmpDir,
93
- });
94
- // 3. Create a minimal mock agent shell script that just outputs JSON
95
- // The step node is already in CAS — the agent just needs to print the JSON line
96
- const mockAgentPath = join(tmpDir, "mock-agent.sh");
97
- const adapterJson = JSON.stringify({
98
- stepHash,
99
- detailHash,
100
- role: "worker",
101
- frontmatter: { $status: "done", result: "test-ok" },
102
- body: "",
103
- startedAtMs,
104
- completedAtMs,
105
- });
106
- await writeFile(mockAgentPath, `#!/bin/sh\necho '${adapterJson}'\n`, { mode: 0o755 });
107
- // 4. Write config.yaml
108
- const configPath = join(tmpDir, "config.yaml");
109
- await writeFile(configPath, `defaultAgent: uwf-hermes\nagentOverrides: null\nagents:\n uwf-hermes:\n command: uwf-hermes\n`);
110
- // 5. Run CLI with agent override pointing to our mock
111
- const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "cli.js");
112
- let stdout;
113
- let stderr;
114
- let exitCode;
115
- try {
116
- stdout = execFileSync(process.execPath, [cliPath, "--format", "raw-json", "thread", "exec", threadId, "--agent", mockAgentPath], {
117
- encoding: "utf8",
118
- stdio: ["ignore", "pipe", "pipe"],
119
- env: {
120
- ...process.env,
121
- UWF_HOME: tmpDir,
122
- OCAS_HOME: casDir,
123
- },
124
- cwd: tmpDir,
125
- timeout: 30000,
126
- });
127
- stderr = "";
128
- exitCode = 0;
129
- }
130
- catch (e) {
131
- const err = e;
132
- stdout = err.stdout ?? "";
133
- stderr = err.stderr ?? "";
134
- exitCode = err.status ?? 1;
135
- }
136
- // 6. Verify
137
- if (exitCode !== 0) {
138
- throw new Error(`CLI exited with code ${exitCode}\nstdout: ${stdout}\nstderr: ${stderr}`);
139
- }
140
- // Parse CLI output (raw-json envelope value: { threadId, workflowHash, steps: [...] })
141
- const cliOutput = JSON.parse(stdout.trim());
142
- expect(cliOutput).toHaveProperty("threadId", threadId);
143
- expect(cliOutput.steps).toHaveLength(1);
144
- const firstStep = cliOutput.steps[0];
145
- expect(firstStep).toHaveProperty("head", stepHash);
146
- expect(firstStep.head).toMatch(/^[0-9A-HJ-NP-TV-Z]{13}$/);
147
- // Verify the CAS step node exists and has correct metadata
148
- const storeAfter = await openStore(casDir);
149
- const stepNode = storeAfter.cas.get(firstStep.head);
150
- expect(stepNode).not.toBeNull();
151
- const payload = stepNode.payload;
152
- expect(payload.role).toBe("worker");
153
- expect(payload.agent).toBe("uwf-mock");
154
- expect(payload.startedAtMs).toBe(1716600000000);
155
- expect(payload.completedAtMs).toBe(1716600001500);
156
- expect(payload.output).toBe(outputHash);
157
- expect(payload.detail).toBe(detailHash);
158
- });
159
- });
160
- //# sourceMappingURL=adapter-json-roundtrip.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter-json-roundtrip.test.js","sourceRoot":"","sources":["../../src/__tests__/adapter-json-roundtrip.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=spawn-agent-json.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"spawn-agent-json.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/spawn-agent-json.test.ts"],"names":[],"mappings":""}
@@ -1,79 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- /**
3
- * B-group tests: validate JSON parsing logic used by spawnAgent.
4
- *
5
- * We test the parsing logic inline since spawnAgent is a private function.
6
- * These tests verify the contract: last line of stdout must be valid JSON
7
- * with a valid stepHash CasRef.
8
- */
9
- const CASREF_PATTERN = /^[0-9A-HJ-NP-TV-Z]{13}$/;
10
- function isCasRef(s) {
11
- return CASREF_PATTERN.test(s);
12
- }
13
- function parseAgentStdout(stdout) {
14
- const line = stdout.trim().split("\n").pop()?.trim() ?? "";
15
- let parsed;
16
- try {
17
- parsed = JSON.parse(line);
18
- }
19
- catch {
20
- throw new Error(`agent stdout last line is not valid JSON: ${line || "(empty)"}`);
21
- }
22
- const obj = parsed;
23
- if (typeof obj !== "object" ||
24
- obj === null ||
25
- typeof obj.stepHash !== "string" ||
26
- !isCasRef(obj.stepHash)) {
27
- throw new Error(`agent stdout JSON missing valid stepHash: ${line}`);
28
- }
29
- return obj;
30
- }
31
- const VALID_OUTPUT = {
32
- stepHash: "0123456789ABC",
33
- detailHash: "DEFGH12345678",
34
- role: "planner",
35
- frontmatter: { $status: "ready", plan: "somehash" },
36
- body: "Plan body",
37
- startedAtMs: 1000,
38
- completedAtMs: 2000,
39
- };
40
- describe("spawnAgent JSON parsing", () => {
41
- test("B1. parses valid JSON from agent stdout", () => {
42
- const stdout = `${JSON.stringify(VALID_OUTPUT)}\n`;
43
- const result = parseAgentStdout(stdout);
44
- expect(result.stepHash).toBe("0123456789ABC");
45
- expect(result.detailHash).toBe("DEFGH12345678");
46
- expect(result.role).toBe("planner");
47
- expect(result.frontmatter).toEqual({ $status: "ready", plan: "somehash" });
48
- expect(result.body).toBe("Plan body");
49
- expect(result.startedAtMs).toBe(1000);
50
- expect(result.completedAtMs).toBe(2000);
51
- });
52
- test("B2. extracts stepHash for head pointer", () => {
53
- const stdout = `${JSON.stringify(VALID_OUTPUT)}\n`;
54
- const result = parseAgentStdout(stdout);
55
- expect(result.stepHash).toBe("0123456789ABC");
56
- expect(isCasRef(result.stepHash)).toBe(true);
57
- });
58
- test("B3. handles debug lines before JSON", () => {
59
- const debugLines = "[debug] loading context...\n[debug] running agent...\n";
60
- const stdout = `${debugLines + JSON.stringify(VALID_OUTPUT)}\n`;
61
- const result = parseAgentStdout(stdout);
62
- expect(result.stepHash).toBe("0123456789ABC");
63
- });
64
- test("B4. rejects non-JSON last line", () => {
65
- const stdout = "not-json-at-all\n";
66
- expect(() => parseAgentStdout(stdout)).toThrow("not valid JSON");
67
- });
68
- test("B5. rejects JSON missing stepHash", () => {
69
- const incomplete = { detailHash: "DEFGH12345678", role: "planner" };
70
- const stdout = `${JSON.stringify(incomplete)}\n`;
71
- expect(() => parseAgentStdout(stdout)).toThrow("missing valid stepHash");
72
- });
73
- test("B6. rejects JSON with invalid stepHash", () => {
74
- const bad = { ...VALID_OUTPUT, stepHash: "not-a-hash" };
75
- const stdout = `${JSON.stringify(bad)}\n`;
76
- expect(() => parseAgentStdout(stdout)).toThrow("missing valid stepHash");
77
- });
78
- });
79
- //# sourceMappingURL=spawn-agent-json.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"spawn-agent-json.test.js","sourceRoot":"","sources":["../../src/__tests__/spawn-agent-json.test.ts"],"names":[],"mappings":"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"}