ftown-bridge 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ /**
2
+ * ftown-workflows engine.
3
+ *
4
+ * Brings Workflow-tool-style deterministic orchestration to REAL ftown sessions:
5
+ * each `agent()` call spawns a real ftown session via the bridge loopback API, the
6
+ * child writes its result to a file, the runner polls the filesystem for that file
7
+ * (race-free — no inbox / Stop-hook contention), removes the session and returns it.
8
+ *
9
+ * The engine is fully dependency-injected (BridgeClient / ResultStore / Clock /
10
+ * Logger) so it is unit-testable without a live bridge, real fs or real timers.
11
+ */
12
+ /** Minimal session-control surface the engine needs. Real impl = HTTP loopback API. */
13
+ export interface BridgeClient {
14
+ /** Create a session; returns its id. Mirrors POST /api/sessions. */
15
+ createSession(opts: SpawnSpec): Promise<{
16
+ id: string;
17
+ }>;
18
+ /** Remove (tombstone) a session. Mirrors DELETE /api/sessions/:id. Must not throw on 404. */
19
+ removeSession(id: string): Promise<void>;
20
+ /** True if the session PTY is alive. Mirrors GET /api/sessions/:id/running. */
21
+ isRunning(id: string): Promise<boolean>;
22
+ }
23
+ /** Reads result files written by child sessions. Real impl = node:fs. */
24
+ export interface ResultStore {
25
+ /** Absolute path where step `stepKey` of `runId` writes its result. */
26
+ resultPath(runId: string, stepKey: string): string;
27
+ /**
28
+ * Returns the parsed RawResult if the file exists AND is valid JSON matching
29
+ * RawResult; returns null if the file is absent OR not yet valid JSON (partial
30
+ * write). MUST NOT throw for absent/partial files.
31
+ */
32
+ readResult(path: string): Promise<RawResult | null>;
33
+ }
34
+ /** Time abstraction so tests run instantly (fake clock). */
35
+ export interface Clock {
36
+ now(): number;
37
+ sleep(ms: number): Promise<void>;
38
+ }
39
+ export interface Logger {
40
+ /** Emitted for phase()/log() and lifecycle events. */
41
+ event(ev: WorkflowEvent): void;
42
+ }
43
+ export type WorkflowEvent = {
44
+ kind: 'phase';
45
+ title: string;
46
+ } | {
47
+ kind: 'log';
48
+ message: string;
49
+ } | {
50
+ kind: 'agent-start';
51
+ label: string;
52
+ phase?: string;
53
+ sessionId: string;
54
+ } | {
55
+ kind: 'agent-done';
56
+ label: string;
57
+ phase?: string;
58
+ ok: boolean;
59
+ cached: boolean;
60
+ } | {
61
+ kind: 'agent-error';
62
+ label: string;
63
+ phase?: string;
64
+ error: string;
65
+ };
66
+ export type WorkflowShell = 'claude' | 'cursor' | 'codex' | 'opencode' | 'shell';
67
+ /** What the engine asks BridgeClient to create. */
68
+ export interface SpawnSpec {
69
+ prompt: string;
70
+ shellType: WorkflowShell;
71
+ workingDir?: string;
72
+ name?: string;
73
+ model?: string;
74
+ parentSessionId: string;
75
+ }
76
+ /** Shape the child writes into its result file. */
77
+ export interface RawResult {
78
+ ok: boolean;
79
+ result?: unknown;
80
+ error?: string;
81
+ }
82
+ export interface AgentOptions {
83
+ label?: string;
84
+ phase?: string;
85
+ schema?: object;
86
+ shell?: WorkflowShell;
87
+ model?: string;
88
+ workdir?: string;
89
+ name?: string;
90
+ timeoutMs?: number;
91
+ pollIntervalMs?: number;
92
+ startupGraceMs?: number;
93
+ }
94
+ export interface Budget {
95
+ /** Max total agent() spawns allowed this run (null = unbounded). */
96
+ maxAgents: number | null;
97
+ /** Count of agent() spawns started so far (cached results do NOT count). */
98
+ spent(): number;
99
+ /** maxAgents - spent(), or Infinity if maxAgents is null. */
100
+ remaining(): number;
101
+ }
102
+ export interface WorkflowContext {
103
+ /**
104
+ * Spawn a real ftown session for `prompt`, block until its result file appears,
105
+ * remove the session, and return the result.
106
+ * - without schema: returns the result as a string. A string `result` is returned
107
+ * as-is; a non-string `result` is returned as a JSON string (JSON.stringify).
108
+ * - with schema: returns the parsed RawResult.result (validated as JSON; on parse
109
+ * failure the agent is treated as failed).
110
+ * Returns null if: the session exits without writing a valid result, the timeout
111
+ * elapses, ok===false, or the budget is exhausted. NEVER rejects for these — only
112
+ * programming errors (bad args) throw.
113
+ */
114
+ agent(prompt: string, opts?: AgentOptions): Promise<string | unknown | null>;
115
+ /** Run thunks concurrently (BARRIER, respects maxConcurrent). A thunk that throws
116
+ * or whose agent errors resolves to null in the result array; the call never rejects. */
117
+ parallel<T>(thunks: Array<() => Promise<T>>): Promise<Array<T | null>>;
118
+ /** Run each item through all stages independently, NO barrier between stages.
119
+ * Stage callback signature: (prev, originalItem, index). A stage that throws drops
120
+ * that item to null and skips its remaining stages. */
121
+ pipeline(items: unknown[], ...stages: Array<(prev: unknown, item: unknown, index: number) => Promise<unknown>>): Promise<unknown[]>;
122
+ phase(title: string): void;
123
+ log(message: string): void;
124
+ readonly args: unknown;
125
+ readonly budget: Budget;
126
+ }
127
+ export interface RunOptions {
128
+ runId: string;
129
+ selfSessionId: string;
130
+ args?: unknown;
131
+ workdir?: string;
132
+ defaultShell?: WorkflowShell;
133
+ defaultTimeoutMs?: number;
134
+ maxConcurrent?: number;
135
+ maxAgents?: number | null;
136
+ }
137
+ export interface RunnerDeps {
138
+ bridge: BridgeClient;
139
+ store: ResultStore;
140
+ clock: Clock;
141
+ logger: Logger;
142
+ }
143
+ /** A loaded workflow module: default export is the script body fn, or a `run` export. */
144
+ export type WorkflowModule = {
145
+ default?: (ctx: WorkflowContext) => Promise<unknown> | unknown;
146
+ run?: (ctx: WorkflowContext) => Promise<unknown> | unknown;
147
+ };
148
+ /**
149
+ * Build the full child prompt: the user's `task`, then a clearly delimited protocol
150
+ * block instructing the child to write its final result as JSON to `resultFilePath`
151
+ * and then stop. When `schema` is provided, the block embeds the schema and says
152
+ * `result` must conform. The block always literally contains `resultFilePath`.
153
+ */
154
+ export declare function buildAgentPrompt(task: string, resultFilePath: string, schema?: object): string;
155
+ /**
156
+ * Parse raw file text into RawResult. Returns null if text is empty or not valid
157
+ * JSON (partial write). Throws nothing. If JSON parses but lacks a boolean `ok`,
158
+ * treat as null (not yet complete).
159
+ */
160
+ export declare function parseResultFile(text: string): RawResult | null;
161
+ /** Build + run the context, execute the module, return its return value. */
162
+ export declare function runWorkflow(deps: RunnerDeps, mod: WorkflowModule, opts: RunOptions): Promise<unknown>;
@@ -0,0 +1,305 @@
1
+ /**
2
+ * ftown-workflows engine.
3
+ *
4
+ * Brings Workflow-tool-style deterministic orchestration to REAL ftown sessions:
5
+ * each `agent()` call spawns a real ftown session via the bridge loopback API, the
6
+ * child writes its result to a file, the runner polls the filesystem for that file
7
+ * (race-free — no inbox / Stop-hook contention), removes the session and returns it.
8
+ *
9
+ * The engine is fully dependency-injected (BridgeClient / ResultStore / Clock /
10
+ * Logger) so it is unit-testable without a live bridge, real fs or real timers.
11
+ */
12
+ const DEFAULT_SHELL = 'claude';
13
+ const DEFAULT_TIMEOUT_MS = 1_800_000;
14
+ const DEFAULT_MAX_CONCURRENT = 4;
15
+ const DEFAULT_POLL_INTERVAL_MS = 2000;
16
+ // A new session reports running:false for a short startup window; only after this grace
17
+ // (with the child never observed running) do we treat it as failed-to-start.
18
+ const DEFAULT_STARTUP_GRACE_MS = 30_000;
19
+ // ---- Pure helpers (exported + individually unit-tested) ----
20
+ /**
21
+ * Build the full child prompt: the user's `task`, then a clearly delimited protocol
22
+ * block instructing the child to write its final result as JSON to `resultFilePath`
23
+ * and then stop. When `schema` is provided, the block embeds the schema and says
24
+ * `result` must conform. The block always literally contains `resultFilePath`.
25
+ */
26
+ export function buildAgentPrompt(task, resultFilePath, schema) {
27
+ const lines = [
28
+ task.trim(),
29
+ '',
30
+ '--- ftown-workflows RESULT PROTOCOL (read carefully) ---',
31
+ 'You are a worker session in a deterministic workflow. When you have finished the',
32
+ 'task above, you MUST write your FINAL result as a single JSON object to this file:',
33
+ '',
34
+ ` RESULT FILE: ${resultFilePath}`,
35
+ '',
36
+ 'The JSON object must have this exact shape:',
37
+ ' { "ok": true, "result": <your result> } on success',
38
+ ' { "ok": false, "error": "<why it failed>" } on failure',
39
+ '',
40
+ ];
41
+ if (schema) {
42
+ lines.push('On success, `result` MUST be valid JSON conforming to this JSON schema:', '', JSON.stringify(schema, null, 2), '');
43
+ }
44
+ else {
45
+ lines.push('On success, `result` may be a plain string or any JSON value. (Without a schema, a', 'non-string `result` is returned to the calling script as a JSON string.)', '');
46
+ }
47
+ lines.push('Write the file atomically (write fully, then save). Do NOT print the result to the', 'terminal instead of the file.', 'The result FILE is the ONLY accepted output channel. Do NOT report via ftown-harness mail, the terminal, or any other channel.', 'After the file is written, STOP — do no further work.', `Reminder: the result file path is ${resultFilePath}`, '--- end protocol ---');
48
+ return lines.join('\n');
49
+ }
50
+ /**
51
+ * Parse raw file text into RawResult. Returns null if text is empty or not valid
52
+ * JSON (partial write). Throws nothing. If JSON parses but lacks a boolean `ok`,
53
+ * treat as null (not yet complete).
54
+ */
55
+ export function parseResultFile(text) {
56
+ if (!text || text.trim().length === 0)
57
+ return null;
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(text);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
66
+ return null;
67
+ const obj = parsed;
68
+ if (typeof obj.ok !== 'boolean')
69
+ return null;
70
+ return obj;
71
+ }
72
+ // ---- Internal helpers ----
73
+ /** Async counting semaphore: caps concurrently-running spawn+poll cycles. */
74
+ class Semaphore {
75
+ permits;
76
+ waiters = [];
77
+ constructor(permits) {
78
+ this.permits = Number.isFinite(permits) ? Math.max(1, Math.floor(permits)) : 1;
79
+ }
80
+ async acquire() {
81
+ if (this.permits > 0) {
82
+ this.permits -= 1;
83
+ return;
84
+ }
85
+ await new Promise((resolve) => this.waiters.push(resolve));
86
+ }
87
+ release() {
88
+ const next = this.waiters.shift();
89
+ if (next) {
90
+ // Hand the permit directly to the next waiter (keeps the cap exact).
91
+ next();
92
+ }
93
+ else {
94
+ this.permits += 1;
95
+ }
96
+ }
97
+ }
98
+ /** Make a label filesystem-safe for use as a result-file stem. */
99
+ function sanitizeStepKey(label) {
100
+ const cleaned = label.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
101
+ return cleaned.length > 0 ? cleaned : 'step';
102
+ }
103
+ /** Map a successful RawResult to the value agent() returns. */
104
+ function mapOk(raw, hasSchema) {
105
+ const value = raw.result;
106
+ if (hasSchema)
107
+ return value ?? null;
108
+ if (value === undefined || value === null)
109
+ return null;
110
+ // No schema: a string is returned as-is; a non-string structured value is
111
+ // JSON-stringified (NOT lossily String()-coerced to "[object Object]").
112
+ return typeof value === 'string' ? value : JSON.stringify(value);
113
+ }
114
+ /** Build + run the context, execute the module, return its return value. */
115
+ export async function runWorkflow(deps, mod, opts) {
116
+ const body = mod.default ?? mod.run;
117
+ if (typeof body !== 'function') {
118
+ throw new Error('workflow module has no default or `run` export');
119
+ }
120
+ const { bridge, store, clock, logger } = deps;
121
+ const defaultShell = opts.defaultShell ?? DEFAULT_SHELL;
122
+ // Resolve numerics defensively: `??` does NOT catch NaN, and a NaN cap would
123
+ // deadlock the semaphore / make every timeout comparison false. Coerce non-finite
124
+ // values to the documented defaults.
125
+ const maxConcurrent = Number.isFinite(opts.maxConcurrent)
126
+ ? Math.max(1, Math.floor(opts.maxConcurrent))
127
+ : DEFAULT_MAX_CONCURRENT;
128
+ const defaultTimeoutMs = Number.isFinite(opts.defaultTimeoutMs)
129
+ ? Math.max(0, opts.defaultTimeoutMs)
130
+ : DEFAULT_TIMEOUT_MS;
131
+ // Number.isFinite(null) === false, so both NaN and null collapse to null (unbounded).
132
+ const maxAgents = Number.isFinite(opts.maxAgents)
133
+ ? Math.max(0, Math.floor(opts.maxAgents))
134
+ : null;
135
+ const sem = new Semaphore(maxConcurrent);
136
+ // Per-run step-key state (drives resumability + dedup).
137
+ const usedKeys = new Set();
138
+ let agentSeq = 0;
139
+ // Budget accounting — only NON-cached spawns count.
140
+ let spentCount = 0;
141
+ function deriveStepKey(label) {
142
+ agentSeq += 1;
143
+ const base = sanitizeStepKey(label ?? `step-${agentSeq}`);
144
+ let key = base;
145
+ let n = 2;
146
+ while (usedKeys.has(key)) {
147
+ key = `${base}-${n}`;
148
+ n += 1;
149
+ }
150
+ usedKeys.add(key);
151
+ return key;
152
+ }
153
+ const budget = {
154
+ maxAgents,
155
+ spent: () => spentCount,
156
+ remaining: () => (maxAgents === null ? Infinity : maxAgents - spentCount),
157
+ };
158
+ /** Poll the result file until it appears, the session dies, or we hit the cap. */
159
+ async function pollForResult(sessionId, path, pollIntervalMs, timeoutMs, startupGraceMs) {
160
+ const start = clock.now();
161
+ // A freshly-created session reports `running: false` for a brief startup window
162
+ // (the PTY exists but the child process is not registered as running yet). If we
163
+ // treated that first `!running` as terminal we would kill the child before it ever
164
+ // booted. So `!running` is only terminal once the child has been observed running
165
+ // at least once (it ran, then exited), or once the startup grace has elapsed
166
+ // without it ever coming up (it failed to start).
167
+ let everRunning = false;
168
+ for (;;) {
169
+ const found = await store.readResult(path);
170
+ if (found)
171
+ return found;
172
+ const elapsed = clock.now() - start;
173
+ if (elapsed >= timeoutMs)
174
+ return null;
175
+ const running = await bridge.isRunning(sessionId);
176
+ if (running) {
177
+ everRunning = true;
178
+ }
179
+ else if (everRunning || elapsed >= startupGraceMs) {
180
+ // Ran-then-exited (do one final read for the wrote-then-exited race) or
181
+ // never started within the grace window.
182
+ return await store.readResult(path);
183
+ }
184
+ await clock.sleep(pollIntervalMs);
185
+ }
186
+ }
187
+ async function agent(prompt, agentOpts = {}) {
188
+ if (typeof prompt !== 'string' || prompt.length === 0) {
189
+ throw new Error('agent(): prompt must be a non-empty string');
190
+ }
191
+ const stepKey = deriveStepKey(agentOpts.label);
192
+ const label = agentOpts.label ?? stepKey;
193
+ const phase = agentOpts.phase;
194
+ const hasSchema = agentOpts.schema !== undefined;
195
+ const path = store.resultPath(opts.runId, stepKey);
196
+ // 1. Resume: a valid result already on disk → return without spawning.
197
+ // A failing cache read is treated as a MISS (proceed to spawn) so agent()
198
+ // honors its never-rejects contract; the poll read inside the try block
199
+ // surfaces real store failures as null.
200
+ let cached = null;
201
+ try {
202
+ cached = await store.readResult(path);
203
+ }
204
+ catch {
205
+ cached = null;
206
+ }
207
+ if (cached) {
208
+ logger.event({ kind: 'agent-done', label, phase, ok: cached.ok, cached: true });
209
+ return cached.ok ? mapOk(cached, hasSchema) : null;
210
+ }
211
+ // 7. Budget: a set cap blocks further spawns once exhausted.
212
+ if (maxAgents !== null && spentCount >= maxAgents) {
213
+ logger.event({ kind: 'agent-error', label, phase, error: 'budget exhausted' });
214
+ return null;
215
+ }
216
+ spentCount += 1;
217
+ const spec = {
218
+ prompt: buildAgentPrompt(prompt, path, agentOpts.schema),
219
+ shellType: agentOpts.shell ?? defaultShell,
220
+ parentSessionId: opts.selfSessionId,
221
+ };
222
+ if (agentOpts.workdir ?? opts.workdir)
223
+ spec.workingDir = agentOpts.workdir ?? opts.workdir;
224
+ spec.name = agentOpts.name ?? label;
225
+ if (agentOpts.model)
226
+ spec.model = agentOpts.model;
227
+ // 6. Concurrency: only `maxConcurrent` spawn+poll cycles run at once.
228
+ await sem.acquire();
229
+ let sessionId;
230
+ try {
231
+ const session = await bridge.createSession(spec);
232
+ sessionId = session.id;
233
+ logger.event({ kind: 'agent-start', label, phase, sessionId });
234
+ const pollInterval = Number.isFinite(agentOpts.pollIntervalMs)
235
+ ? Math.max(50, agentOpts.pollIntervalMs)
236
+ : DEFAULT_POLL_INTERVAL_MS;
237
+ const startupGrace = Number.isFinite(agentOpts.startupGraceMs)
238
+ ? Math.max(0, agentOpts.startupGraceMs)
239
+ : DEFAULT_STARTUP_GRACE_MS;
240
+ const raw = await pollForResult(sessionId, path, pollInterval, agentOpts.timeoutMs ?? defaultTimeoutMs, startupGrace);
241
+ if (raw && raw.ok) {
242
+ logger.event({ kind: 'agent-done', label, phase, ok: true, cached: false });
243
+ return mapOk(raw, hasSchema);
244
+ }
245
+ const error = raw
246
+ ? (raw.error ?? 'agent reported failure')
247
+ : 'no result (session exited or timed out)';
248
+ logger.event({ kind: 'agent-error', label, phase, error });
249
+ return null;
250
+ }
251
+ catch (err) {
252
+ // Real bridge/store errors must not reject agent() — map to a failure.
253
+ const message = err instanceof Error ? err.message : String(err);
254
+ logger.event({ kind: 'agent-error', label, phase, error: message });
255
+ return null;
256
+ }
257
+ finally {
258
+ // 5. Cleanup: ALWAYS remove the session, swallowing 404s.
259
+ if (sessionId !== undefined) {
260
+ try {
261
+ await bridge.removeSession(sessionId);
262
+ }
263
+ catch {
264
+ /* removeSession must not throw on 404 — ignore. */
265
+ }
266
+ }
267
+ sem.release();
268
+ }
269
+ }
270
+ async function parallel(thunks) {
271
+ return Promise.all(thunks.map(async (thunk) => {
272
+ try {
273
+ return await thunk();
274
+ }
275
+ catch {
276
+ return null;
277
+ }
278
+ }));
279
+ }
280
+ async function pipeline(items, ...stages) {
281
+ return Promise.all(items.map(async (item, index) => {
282
+ let prev = item;
283
+ try {
284
+ for (const stage of stages) {
285
+ prev = await stage(prev, item, index);
286
+ }
287
+ return prev;
288
+ }
289
+ catch {
290
+ return null;
291
+ }
292
+ }));
293
+ }
294
+ const ctx = {
295
+ agent,
296
+ parallel,
297
+ pipeline,
298
+ phase: (title) => logger.event({ kind: 'phase', title }),
299
+ log: (message) => logger.event({ kind: 'log', message }),
300
+ args: opts.args,
301
+ budget,
302
+ };
303
+ return await body(ctx);
304
+ }
305
+ //# sourceMappingURL=workflow-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-runner.js","sourceRoot":"","sources":["../src/workflow-runner.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "ftown-bridge",
3
- "version": "0.9.3",
4
- "description": "CLI bridge for ftown \u2014 generic PTY-over-Centrifugo relay",
3
+ "version": "0.9.4",
4
+ "description": "CLI bridge for ftown generic PTY-over-Centrifugo relay",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "ftown-bridge": "dist/index.js",
9
9
  "ftown-harness": "dist/harness-cli.js",
10
- "ftown-sessions": "bin/ftown-sessions"
10
+ "ftown-sessions": "bin/ftown-sessions",
11
+ "ftown-workflows": "dist/workflow-runner-cli.js"
11
12
  },
12
13
  "files": [
13
14
  "dist",
@@ -17,6 +18,7 @@
17
18
  ],
18
19
  "scripts": {
19
20
  "build": "tsc",
21
+ "test": "node --import tsx --test \"src/**/*.test.ts\"",
20
22
  "prepublishOnly": "npm run build",
21
23
  "start": "tsx src/index.ts",
22
24
  "dev": "tsx watch src/index.ts",