@united-workforce/cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/dist/__tests__/adapter-json-roundtrip.test.js +1 -1
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts +2 -0
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts.map +1 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js +30 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -0
- package/dist/__tests__/build-step-entry.test.d.ts +2 -0
- package/dist/__tests__/build-step-entry.test.d.ts.map +1 -0
- package/dist/__tests__/build-step-entry.test.js +173 -0
- package/dist/__tests__/build-step-entry.test.js.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts +2 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js +93 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js.map +1 -0
- package/dist/__tests__/config.test.js +26 -302
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/current-role.test.js +7 -6
- package/dist/__tests__/current-role.test.js.map +1 -1
- package/dist/__tests__/e2e-mock-agent.test.js +20 -23
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts +2 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts.map +1 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js +40 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.js +9 -50
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
- package/dist/__tests__/pid-recycling.test.d.ts +2 -0
- package/dist/__tests__/pid-recycling.test.d.ts.map +1 -0
- package/dist/__tests__/pid-recycling.test.js +271 -0
- package/dist/__tests__/pid-recycling.test.js.map +1 -0
- package/dist/__tests__/prompt.test.js +321 -0
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +4 -4
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
- package/dist/__tests__/setup-agent-discovery.test.js +21 -30
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
- package/dist/__tests__/setup-complexity.test.js +2 -168
- package/dist/__tests__/setup-complexity.test.js.map +1 -1
- package/dist/__tests__/setup-no-llm.test.d.ts +2 -0
- package/dist/__tests__/setup-no-llm.test.d.ts.map +1 -0
- package/dist/__tests__/setup-no-llm.test.js +52 -0
- package/dist/__tests__/setup-no-llm.test.js.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js +24 -27
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.d.ts +2 -0
- package/dist/__tests__/step-ask.test.d.ts.map +1 -0
- package/dist/__tests__/step-ask.test.js +499 -0
- package/dist/__tests__/step-ask.test.js.map +1 -0
- package/dist/__tests__/step-show-json.test.js +1 -0
- package/dist/__tests__/step-show-json.test.js.map +1 -1
- package/dist/__tests__/step-timing.test.js +2 -0
- package/dist/__tests__/step-timing.test.js.map +1 -1
- package/dist/__tests__/store-global-cas.test.js +2 -2
- package/dist/__tests__/store-global-cas.test.js.map +1 -1
- package/dist/__tests__/store-unified-threads.test.js +9 -9
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-status.test.js +6 -6
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
- package/dist/__tests__/thread-list-filters.test.js +344 -9
- package/dist/__tests__/thread-list-filters.test.js.map +1 -1
- package/dist/__tests__/thread-poke.test.d.ts +2 -0
- package/dist/__tests__/thread-poke.test.d.ts.map +1 -0
- package/dist/__tests__/thread-poke.test.js +412 -0
- package/dist/__tests__/thread-poke.test.js.map +1 -0
- package/dist/__tests__/thread-resume.test.js +10 -14
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-show-status.test.js +17 -28
- package/dist/__tests__/thread-show-status.test.js.map +1 -1
- package/dist/__tests__/thread-suspend-step.test.js +8 -14
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread-suspended-display.test.js +10 -22
- package/dist/__tests__/thread-suspended-display.test.js.map +1 -1
- package/dist/__tests__/thread.test.js +4 -4
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +49 -21
- package/dist/__tests__/validate-semantic.test.js.map +1 -1
- package/dist/__tests__/workflow-list-recursive.test.d.ts +2 -0
- package/dist/__tests__/workflow-list-recursive.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-list-recursive.test.js +283 -0
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +36 -21
- package/dist/__tests__/workflow-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-show-resolution.test.d.ts +2 -0
- package/dist/__tests__/workflow-show-resolution.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-show-resolution.test.js +210 -0
- package/dist/__tests__/workflow-show-resolution.test.js.map +1 -0
- package/dist/__tests__/workflow-validate.test.d.ts +2 -0
- package/dist/__tests__/workflow-validate.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-validate.test.js +687 -0
- package/dist/__tests__/workflow-validate.test.js.map +1 -0
- package/dist/background/background.d.ts +22 -1
- package/dist/background/background.d.ts.map +1 -1
- package/dist/background/background.js +83 -6
- package/dist/background/background.js.map +1 -1
- package/dist/background/index.d.ts +1 -1
- package/dist/background/index.d.ts.map +1 -1
- package/dist/background/index.js +1 -1
- package/dist/background/index.js.map +1 -1
- package/dist/background/types.d.ts +1 -0
- package/dist/background/types.d.ts.map +1 -1
- package/dist/cli.js +66 -31
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts +3 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +7 -33
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +15 -2
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +7 -39
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +27 -302
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +44 -1
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +255 -11
- package/dist/commands/step.js.map +1 -1
- package/dist/commands/thread.d.ts +16 -3
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +379 -140
- package/dist/commands/thread.js.map +1 -1
- package/dist/commands/workflow.d.ts +9 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +130 -6
- package/dist/commands/workflow.js.map +1 -1
- package/dist/moderator/__tests__/evaluate.test.js +31 -17
- package/dist/moderator/__tests__/evaluate.test.js.map +1 -1
- package/dist/moderator/evaluate.d.ts.map +1 -1
- package/dist/moderator/evaluate.js +4 -16
- package/dist/moderator/evaluate.js.map +1 -1
- package/dist/moderator/index.d.ts +1 -2
- package/dist/moderator/index.d.ts.map +1 -1
- package/dist/moderator/index.js +0 -1
- package/dist/moderator/index.js.map +1 -1
- package/dist/moderator/types.d.ts +6 -10
- package/dist/moderator/types.d.ts.map +1 -1
- package/dist/moderator/types.js +1 -3
- package/dist/moderator/types.js.map +1 -1
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +5 -3
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +28 -9
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +75 -16
- package/dist/store.js.map +1 -1
- package/dist/validate-semantic.d.ts.map +1 -1
- package/dist/validate-semantic.js +83 -66
- package/dist/validate-semantic.js.map +1 -1
- package/dist/validate.d.ts +6 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +24 -0
- package/dist/validate.js.map +1 -1
- package/package.json +8 -10
- package/src/__tests__/adapter-json-roundtrip.test.ts +1 -1
- package/src/__tests__/agent-resolution-llm-free.test.ts +39 -0
- package/src/__tests__/build-step-entry.test.ts +203 -0
- package/src/__tests__/clear-thread-failed-attempts.test.ts +122 -0
- package/src/__tests__/config.test.ts +33 -321
- package/src/__tests__/current-role.test.ts +7 -6
- package/src/__tests__/e2e-mock-agent.test.ts +20 -23
- package/src/__tests__/fixtures/e2e-count.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-linear.workflow.yaml +1 -0
- package/src/__tests__/fixtures/{e2e-mustache.workflow.yaml → e2e-liquid.workflow.yaml} +3 -2
- package/src/__tests__/fixtures/e2e-loop.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-suspend.mock.yaml +2 -2
- package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +6 -10
- package/src/__tests__/issue-180-workflow-ref-removed.test.ts +43 -0
- package/src/__tests__/moderator-evaluate.test.ts +9 -52
- package/src/__tests__/pid-recycling.test.ts +328 -0
- package/src/__tests__/prompt.test.ts +397 -0
- package/src/__tests__/resolve-head-hash.test.ts +4 -4
- package/src/__tests__/setup-agent-discovery.test.ts +26 -51
- package/src/__tests__/setup-complexity.test.ts +1 -203
- package/src/__tests__/setup-no-llm.test.ts +68 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +24 -30
- package/src/__tests__/step-ask.test.ts +670 -0
- package/src/__tests__/step-show-json.test.ts +1 -0
- package/src/__tests__/step-timing.test.ts +2 -0
- package/src/__tests__/store-global-cas.test.ts +2 -2
- package/src/__tests__/store-unified-threads.test.ts +9 -9
- package/src/__tests__/thread-cancel-status.test.ts +6 -6
- package/src/__tests__/thread-list-filters.test.ts +434 -8
- package/src/__tests__/thread-poke.test.ts +545 -0
- package/src/__tests__/thread-resume.test.ts +10 -14
- package/src/__tests__/thread-show-status.test.ts +17 -29
- package/src/__tests__/thread-suspend-step.test.ts +8 -14
- package/src/__tests__/thread-suspended-display.test.ts +10 -22
- package/src/__tests__/thread.test.ts +4 -4
- package/src/__tests__/validate-semantic.test.ts +59 -31
- package/src/__tests__/workflow-list-recursive.test.ts +370 -0
- package/src/__tests__/workflow-resolution.test.ts +39 -21
- package/src/__tests__/workflow-show-resolution.test.ts +285 -0
- package/src/__tests__/workflow-validate.test.ts +806 -0
- package/src/background/background.ts +88 -6
- package/src/background/index.ts +2 -0
- package/src/background/types.ts +1 -0
- package/src/cli.ts +97 -47
- package/src/commands/config.ts +7 -35
- package/src/commands/prompt.ts +15 -2
- package/src/commands/setup.ts +29 -357
- package/src/commands/step.ts +339 -12
- package/src/commands/thread.ts +463 -169
- package/src/commands/workflow.ts +159 -4
- package/src/moderator/__tests__/evaluate.test.ts +34 -17
- package/src/moderator/evaluate.ts +5 -17
- package/src/moderator/index.ts +1 -6
- package/src/moderator/types.ts +6 -14
- package/src/schemas.ts +13 -3
- package/src/store.ts +86 -20
- package/src/validate-semantic.ts +109 -78
- package/src/validate.ts +27 -0
- package/dist/__tests__/setup-validate.test.d.ts +0 -2
- package/dist/__tests__/setup-validate.test.d.ts.map +0 -1
- package/dist/__tests__/setup-validate.test.js +0 -108
- package/dist/__tests__/setup-validate.test.js.map +0 -1
- package/src/__tests__/setup-validate.test.ts +0 -148
- /package/src/__tests__/fixtures/{e2e-mustache.mock.yaml → e2e-liquid.mock.yaml} +0 -0
package/src/commands/step.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import type { CasStore } from "@ocas/core";
|
|
2
3
|
import type {
|
|
4
|
+
AgentAlias,
|
|
5
|
+
AgentConfig,
|
|
3
6
|
CasRef,
|
|
4
7
|
StartEntry,
|
|
5
8
|
StepEntry,
|
|
@@ -7,9 +10,12 @@ import type {
|
|
|
7
10
|
ThreadForkOutput,
|
|
8
11
|
ThreadId,
|
|
9
12
|
ThreadStepsOutput,
|
|
13
|
+
WorkflowConfig,
|
|
14
|
+
WorkflowPayload,
|
|
10
15
|
} from "@united-workforce/protocol";
|
|
11
|
-
import { generateUlid } from "@united-workforce/util";
|
|
12
|
-
import {
|
|
16
|
+
import { createLogger, generateUlid } from "@united-workforce/util";
|
|
17
|
+
import { getAskSessionId, loadWorkflowConfig, setAskSessionId } from "@united-workforce/util-agent";
|
|
18
|
+
import { createUwfStore, setThread, type UwfStore } from "../store.js";
|
|
13
19
|
import {
|
|
14
20
|
collectOrderedSteps,
|
|
15
21
|
expandDeep,
|
|
@@ -19,6 +25,8 @@ import {
|
|
|
19
25
|
walkChain,
|
|
20
26
|
} from "./shared.js";
|
|
21
27
|
|
|
28
|
+
const log = createLogger({ sink: { kind: "stderr" } });
|
|
29
|
+
|
|
22
30
|
type TurnToolCall = {
|
|
23
31
|
name: string;
|
|
24
32
|
args: string;
|
|
@@ -31,6 +39,117 @@ type TurnData = {
|
|
|
31
39
|
toolCalls: TurnToolCall[] | null;
|
|
32
40
|
};
|
|
33
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Build a StepEntry for a single step CAS hash, recursively populating its
|
|
44
|
+
* `previousAttempts` from prior failed StepNode hashes (if any). Failed steps
|
|
45
|
+
* are persisted to CAS but never reachable through `prev`; they live only via
|
|
46
|
+
* the successful step's `previousAttempts` array.
|
|
47
|
+
*/
|
|
48
|
+
export function buildStepEntry(uwf: UwfStore, stepHash: CasRef): StepEntry | null {
|
|
49
|
+
const node = uwf.store.cas.get(stepHash);
|
|
50
|
+
if (node === null || node.type !== uwf.schemas.stepNode) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const payload = node.payload as StepNodePayload;
|
|
54
|
+
const previousHashes = payload.previousAttempts ?? null;
|
|
55
|
+
let previousAttempts: StepEntry[] | null = null;
|
|
56
|
+
if (previousHashes !== null && previousHashes.length > 0) {
|
|
57
|
+
const entries: StepEntry[] = [];
|
|
58
|
+
for (const prevHash of previousHashes) {
|
|
59
|
+
const entry = buildStepEntry(uwf, prevHash);
|
|
60
|
+
if (entry !== null) {
|
|
61
|
+
entries.push(entry);
|
|
62
|
+
} else {
|
|
63
|
+
log(
|
|
64
|
+
"STP7K2QM",
|
|
65
|
+
`previousAttempts ref ${prevHash} for step ${stepHash} did not resolve to a StepNode; skipping it in retry lineage`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
previousAttempts = entries.length > 0 ? entries : null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
hash: stepHash,
|
|
73
|
+
role: payload.role,
|
|
74
|
+
output: expandOutput(uwf, payload.output),
|
|
75
|
+
detail: payload.detail ?? null,
|
|
76
|
+
agent: payload.agent,
|
|
77
|
+
timestamp: node.timestamp,
|
|
78
|
+
durationMs: payload.completedAtMs - payload.startedAtMs,
|
|
79
|
+
usage: payload.usage ?? null,
|
|
80
|
+
previousAttempts,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sum of usage across an entry and its nested previousAttempts.
|
|
86
|
+
* Treats null usage as zero. Returns a flat aggregate — recursive traversal is
|
|
87
|
+
* internal.
|
|
88
|
+
*/
|
|
89
|
+
export function sumStepEntryUsage(entry: StepEntry): {
|
|
90
|
+
turns: number;
|
|
91
|
+
inputTokens: number;
|
|
92
|
+
outputTokens: number;
|
|
93
|
+
duration: number;
|
|
94
|
+
} {
|
|
95
|
+
let turns = 0;
|
|
96
|
+
let inputTokens = 0;
|
|
97
|
+
let outputTokens = 0;
|
|
98
|
+
let duration = 0;
|
|
99
|
+
if (entry.usage !== null) {
|
|
100
|
+
turns += entry.usage.turns;
|
|
101
|
+
inputTokens += entry.usage.inputTokens;
|
|
102
|
+
outputTokens += entry.usage.outputTokens;
|
|
103
|
+
duration += entry.usage.duration;
|
|
104
|
+
}
|
|
105
|
+
if (entry.previousAttempts !== null) {
|
|
106
|
+
for (const attempt of entry.previousAttempts) {
|
|
107
|
+
const sub = sumStepEntryUsage(attempt);
|
|
108
|
+
turns += sub.turns;
|
|
109
|
+
inputTokens += sub.inputTokens;
|
|
110
|
+
outputTokens += sub.outputTokens;
|
|
111
|
+
duration += sub.duration;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { turns, inputTokens, outputTokens, duration };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Aggregate token usage across the entire thread chain, including any
|
|
119
|
+
* recorded failed retry attempts via `previousAttempts`. Returns zeros when
|
|
120
|
+
* no usage is recorded anywhere on the thread.
|
|
121
|
+
*/
|
|
122
|
+
export async function aggregateThreadUsage(
|
|
123
|
+
storageRoot: string,
|
|
124
|
+
threadId: ThreadId,
|
|
125
|
+
): Promise<{
|
|
126
|
+
turns: number;
|
|
127
|
+
inputTokens: number;
|
|
128
|
+
outputTokens: number;
|
|
129
|
+
duration: number;
|
|
130
|
+
}> {
|
|
131
|
+
const result = await cmdStepList(storageRoot, threadId);
|
|
132
|
+
let turns = 0;
|
|
133
|
+
let inputTokens = 0;
|
|
134
|
+
let outputTokens = 0;
|
|
135
|
+
let duration = 0;
|
|
136
|
+
for (const entry of result.steps) {
|
|
137
|
+
if (!isStepEntry(entry)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const sub = sumStepEntryUsage(entry);
|
|
141
|
+
turns += sub.turns;
|
|
142
|
+
inputTokens += sub.inputTokens;
|
|
143
|
+
outputTokens += sub.outputTokens;
|
|
144
|
+
duration += sub.duration;
|
|
145
|
+
}
|
|
146
|
+
return { turns, inputTokens, outputTokens, duration };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isStepEntry(entry: StartEntry | StepEntry): entry is StepEntry {
|
|
150
|
+
return "role" in entry && "agent" in entry;
|
|
151
|
+
}
|
|
152
|
+
|
|
34
153
|
/**
|
|
35
154
|
* List all steps in a thread (previously: thread steps)
|
|
36
155
|
*/
|
|
@@ -58,16 +177,10 @@ export async function cmdStepList(
|
|
|
58
177
|
const ordered = collectOrderedSteps(uwf, headHash, chain);
|
|
59
178
|
|
|
60
179
|
for (const item of ordered) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
detail: item.payload.detail ?? null,
|
|
66
|
-
agent: item.payload.agent,
|
|
67
|
-
timestamp: item.timestamp,
|
|
68
|
-
durationMs: item.payload.completedAtMs - item.payload.startedAtMs,
|
|
69
|
-
usage: item.payload.usage ?? null,
|
|
70
|
-
});
|
|
180
|
+
const entry = buildStepEntry(uwf, item.hash);
|
|
181
|
+
if (entry !== null) {
|
|
182
|
+
stepEntries.push(entry);
|
|
183
|
+
}
|
|
71
184
|
}
|
|
72
185
|
|
|
73
186
|
return {
|
|
@@ -341,3 +454,217 @@ export async function cmdStepRead(
|
|
|
341
454
|
|
|
342
455
|
return formatStepMarkdown(stepHash, payload.role, payload.agent, turnData, selectedTurns);
|
|
343
456
|
}
|
|
457
|
+
|
|
458
|
+
// ── step ask ────────────────────────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
function parseAgentOverride(override: string): AgentConfig {
|
|
461
|
+
const parts = override
|
|
462
|
+
.trim()
|
|
463
|
+
.split(/\s+/)
|
|
464
|
+
.filter((p) => p.length > 0);
|
|
465
|
+
const command = parts[0];
|
|
466
|
+
if (command === undefined) {
|
|
467
|
+
fail("agent override must not be empty");
|
|
468
|
+
}
|
|
469
|
+
return { command, args: parts.slice(1) };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function resolveAskAgentConfig(
|
|
473
|
+
config: WorkflowConfig,
|
|
474
|
+
workflow: WorkflowPayload | null,
|
|
475
|
+
role: string,
|
|
476
|
+
agentOverride: string | null,
|
|
477
|
+
recordedAgent: string,
|
|
478
|
+
): AgentConfig {
|
|
479
|
+
if (agentOverride !== null) {
|
|
480
|
+
const fromAlias = config.agents[agentOverride as AgentAlias];
|
|
481
|
+
if (fromAlias !== undefined) {
|
|
482
|
+
return fromAlias;
|
|
483
|
+
}
|
|
484
|
+
return parseAgentOverride(agentOverride);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Try to resolve via the recorded agent name as a config alias.
|
|
488
|
+
const fromRecorded = config.agents[recordedAgent as AgentAlias];
|
|
489
|
+
if (fromRecorded !== undefined) {
|
|
490
|
+
return fromRecorded;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Fall back to default agent for the workflow / role.
|
|
494
|
+
if (workflow !== null && config.agentOverrides !== null) {
|
|
495
|
+
const roleOverrides = config.agentOverrides[workflow.name];
|
|
496
|
+
if (roleOverrides !== undefined && roleOverrides[role] !== undefined) {
|
|
497
|
+
const alias = roleOverrides[role];
|
|
498
|
+
const agentConfig = config.agents[alias];
|
|
499
|
+
if (agentConfig !== undefined) {
|
|
500
|
+
return agentConfig;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Treat the recorded value as a raw command path.
|
|
506
|
+
return parseAgentOverride(recordedAgent);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Derive the agent name used for cache file partitioning from an executable
|
|
511
|
+
* path or alias. Examples:
|
|
512
|
+
* uwf-hermes → hermes
|
|
513
|
+
* uwf-claude-code → claude-code
|
|
514
|
+
* /tmp/mock-agent.sh → mock
|
|
515
|
+
* /usr/bin/agent → agent
|
|
516
|
+
*/
|
|
517
|
+
function deriveAgentName(commandPath: string): string {
|
|
518
|
+
const basename = commandPath.split(/[/\\]/).pop() ?? commandPath;
|
|
519
|
+
// Strip a trailing extension (.sh, .js, .mjs, .cjs)
|
|
520
|
+
const noExt = basename.replace(/\.(sh|js|mjs|cjs|ts)$/i, "");
|
|
521
|
+
// Strip the `uwf-` prefix introduced by agentLabel().
|
|
522
|
+
const noPrefix = noExt.startsWith("uwf-") ? noExt.slice(4) : noExt;
|
|
523
|
+
// Strip the trailing `-agent` suffix used by tests / generic agent shells.
|
|
524
|
+
const noSuffix = noPrefix.endsWith("-agent") ? noPrefix.slice(0, -"-agent".length) : noPrefix;
|
|
525
|
+
return noSuffix === "" ? noExt : noSuffix;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function loadDetailNode(
|
|
529
|
+
store: CasStore,
|
|
530
|
+
detailRef: CasRef,
|
|
531
|
+
): { sessionId: string | null; payload: Record<string, unknown> } {
|
|
532
|
+
const detailNode = store.get(detailRef);
|
|
533
|
+
if (detailNode === null) {
|
|
534
|
+
fail(`detail node not found: ${detailRef}`);
|
|
535
|
+
}
|
|
536
|
+
const payload = detailNode.payload as Record<string, unknown>;
|
|
537
|
+
const sessionId = typeof payload.sessionId === "string" ? payload.sessionId : null;
|
|
538
|
+
return { sessionId, payload };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function spawnAskAgent(agent: AgentConfig, argv: string[], cwd: string): { stdout: string } {
|
|
542
|
+
try {
|
|
543
|
+
const stdout = execFileSync(agent.command, [...agent.args, ...argv], {
|
|
544
|
+
encoding: "utf8",
|
|
545
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
546
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
547
|
+
cwd,
|
|
548
|
+
});
|
|
549
|
+
return { stdout };
|
|
550
|
+
} catch (e) {
|
|
551
|
+
const err = e as NodeJS.ErrnoException & { stderr: Buffer | string | null };
|
|
552
|
+
if (err.code === "ENOENT") {
|
|
553
|
+
fail(
|
|
554
|
+
`"${agent.command}" not found in PATH. Install it or check your PATH config. Run: which ${agent.command}`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
const stderr =
|
|
558
|
+
err.stderr == null
|
|
559
|
+
? ""
|
|
560
|
+
: typeof err.stderr === "string"
|
|
561
|
+
? err.stderr
|
|
562
|
+
: err.stderr.toString("utf8");
|
|
563
|
+
const detail = stderr.trim() !== "" ? `: ${stderr.trim()}` : "";
|
|
564
|
+
fail(`agent command failed (${agent.command})${detail}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function resolveAskWorkflow(uwf: UwfStore, payload: StepNodePayload): WorkflowPayload | null {
|
|
569
|
+
const startNode = uwf.store.cas.get(payload.start);
|
|
570
|
+
if (startNode === null) {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
const start = startNode.payload as { workflow: CasRef };
|
|
574
|
+
const workflowNode = uwf.store.cas.get(start.workflow);
|
|
575
|
+
if (workflowNode === null) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
return workflowNode.payload as WorkflowPayload;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function performFork(
|
|
582
|
+
agent: AgentConfig,
|
|
583
|
+
agentName: string,
|
|
584
|
+
stepHash: CasRef,
|
|
585
|
+
sourceSessionId: string,
|
|
586
|
+
storageRoot: string,
|
|
587
|
+
cwd: string,
|
|
588
|
+
): Promise<string> {
|
|
589
|
+
const cached = await getAskSessionId(agentName, stepHash, storageRoot);
|
|
590
|
+
if (cached !== null) {
|
|
591
|
+
return cached;
|
|
592
|
+
}
|
|
593
|
+
const { stdout } = spawnAskAgent(agent, ["--mode", "fork", "--session", sourceSessionId], cwd);
|
|
594
|
+
const newSessionId = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
595
|
+
if (newSessionId === "") {
|
|
596
|
+
fail(`agent fork did not return a session id (${agent.command})`);
|
|
597
|
+
}
|
|
598
|
+
await setAskSessionId(agentName, stepHash, newSessionId, storageRoot);
|
|
599
|
+
return newSessionId;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export type CmdStepAskOptions = {
|
|
603
|
+
prompt: string;
|
|
604
|
+
agentOverride: string | null;
|
|
605
|
+
/** When false, skip session forking and pass detail ref for context injection. */
|
|
606
|
+
fork: boolean;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Ask a follow-up question to a historical step's agent (read-only).
|
|
611
|
+
*
|
|
612
|
+
* Does NOT write a new StepNode and does NOT mutate thread state. The agent's
|
|
613
|
+
* raw stdout is returned so the CLI entry point can stream it directly.
|
|
614
|
+
*/
|
|
615
|
+
export async function cmdStepAsk(
|
|
616
|
+
storageRoot: string,
|
|
617
|
+
stepHash: CasRef,
|
|
618
|
+
options: CmdStepAskOptions,
|
|
619
|
+
): Promise<string> {
|
|
620
|
+
const uwf = await createUwfStore(storageRoot);
|
|
621
|
+
const node = uwf.store.cas.get(stepHash);
|
|
622
|
+
if (node === null) {
|
|
623
|
+
fail(`CAS node not found: ${stepHash}`);
|
|
624
|
+
}
|
|
625
|
+
if (node.type !== uwf.schemas.stepNode) {
|
|
626
|
+
fail(`node ${stepHash} is not a StepNode`);
|
|
627
|
+
}
|
|
628
|
+
const payload = node.payload as StepNodePayload;
|
|
629
|
+
if (payload.detail === null) {
|
|
630
|
+
fail(`step ${stepHash} has no detail; cannot ask`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const detailRef = payload.detail;
|
|
634
|
+
const { sessionId: sourceSessionId } = loadDetailNode(uwf.store.cas, detailRef);
|
|
635
|
+
|
|
636
|
+
const workflow = resolveAskWorkflow(uwf, payload);
|
|
637
|
+
const config = await loadWorkflowConfig(storageRoot);
|
|
638
|
+
const agent = resolveAskAgentConfig(
|
|
639
|
+
config,
|
|
640
|
+
workflow,
|
|
641
|
+
payload.role,
|
|
642
|
+
options.agentOverride,
|
|
643
|
+
payload.agent,
|
|
644
|
+
);
|
|
645
|
+
const agentName = deriveAgentName(agent.command);
|
|
646
|
+
|
|
647
|
+
const cwd = payload.cwd !== "" ? payload.cwd : process.cwd();
|
|
648
|
+
|
|
649
|
+
// Fork path: fork (or reuse cached fork) → ask with that session.
|
|
650
|
+
if (options.fork && sourceSessionId !== null) {
|
|
651
|
+
const askSessionId = await performFork(
|
|
652
|
+
agent,
|
|
653
|
+
agentName,
|
|
654
|
+
stepHash,
|
|
655
|
+
sourceSessionId,
|
|
656
|
+
storageRoot,
|
|
657
|
+
cwd,
|
|
658
|
+
);
|
|
659
|
+
const argv = ["--mode", "ask", "--session", askSessionId, "--prompt", options.prompt];
|
|
660
|
+
argv.push("--detail", detailRef);
|
|
661
|
+
const { stdout } = spawnAskAgent(agent, argv, cwd);
|
|
662
|
+
return stdout;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Fallback path: ask without forking; inject detail ref for context.
|
|
666
|
+
const argv = ["--mode", "ask", "--prompt", options.prompt];
|
|
667
|
+
argv.push("--detail", detailRef);
|
|
668
|
+
const { stdout } = spawnAskAgent(agent, argv, cwd);
|
|
669
|
+
return stdout;
|
|
670
|
+
}
|