@tagma/sdk 0.4.13 → 0.4.15
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/LICENSE +21 -21
- package/README.md +569 -572
- package/dist/dag.d.ts.map +1 -1
- package/dist/dag.js +22 -56
- package/dist/dag.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +63 -37
- package/dist/engine.js.map +1 -1
- package/dist/middlewares/static-context.d.ts.map +1 -1
- package/dist/middlewares/static-context.js +7 -3
- package/dist/middlewares/static-context.js.map +1 -1
- package/dist/prompt-doc.d.ts +36 -0
- package/dist/prompt-doc.d.ts.map +1 -0
- package/dist/prompt-doc.js +44 -0
- package/dist/prompt-doc.js.map +1 -0
- package/dist/sdk.d.ts +3 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +4 -0
- package/dist/sdk.js.map +1 -1
- package/dist/task-ref.d.ts +55 -0
- package/dist/task-ref.d.ts.map +1 -0
- package/dist/task-ref.js +101 -0
- package/dist/task-ref.js.map +1 -0
- package/dist/templates.d.ts +20 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +93 -0
- package/dist/templates.js.map +1 -0
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +27 -53
- package/dist/validate-raw.js.map +1 -1
- package/package.json +2 -2
- package/scripts/preinstall.js +31 -31
- package/src/adapters/stdin-approval.ts +106 -106
- package/src/adapters/websocket-approval.ts +224 -224
- package/src/approval.ts +131 -131
- package/src/bootstrap.ts +37 -37
- package/src/completions/exit-code.ts +34 -34
- package/src/completions/file-exists.ts +66 -66
- package/src/completions/output-check.ts +86 -86
- package/src/config-ops.ts +307 -307
- package/src/dag.ts +24 -54
- package/src/drivers/claude-code.ts +250 -250
- package/src/engine.ts +1137 -1098
- package/src/hooks.ts +187 -187
- package/src/logger.ts +182 -182
- package/src/middlewares/static-context.ts +49 -45
- package/src/pipeline-runner.ts +156 -156
- package/src/prompt-doc.ts +49 -0
- package/src/registry.ts +242 -242
- package/src/runner.ts +395 -395
- package/src/schema.test.ts +101 -101
- package/src/schema.ts +338 -338
- package/src/sdk.ts +111 -92
- package/src/task-ref.ts +120 -0
- package/src/triggers/file.ts +164 -164
- package/src/triggers/manual.ts +86 -86
- package/src/types.ts +18 -18
- package/src/utils.ts +203 -203
- package/src/validate-raw.ts +412 -442
package/src/pipeline-runner.ts
CHANGED
|
@@ -1,156 +1,156 @@
|
|
|
1
|
-
// ═══ PipelineRunner ═══
|
|
2
|
-
//
|
|
3
|
-
// Wraps runPipeline in a lifecycle object suited for multi-pipeline management
|
|
4
|
-
// in sidecar / Tauri IPC scenarios. Each instance controls one pipeline run.
|
|
5
|
-
//
|
|
6
|
-
// Typical sidecar usage:
|
|
7
|
-
//
|
|
8
|
-
// const runners = new Map<string, PipelineRunner>();
|
|
9
|
-
//
|
|
10
|
-
// const runner = new PipelineRunner(config, workDir);
|
|
11
|
-
// runner.subscribe(event => ipcEmit('pipeline_event', event));
|
|
12
|
-
// runner.start();
|
|
13
|
-
// runners.set(runner.instanceId, runner);
|
|
14
|
-
//
|
|
15
|
-
// // Later, from IPC:
|
|
16
|
-
// runners.get(id)?.abort();
|
|
17
|
-
|
|
18
|
-
import { runPipeline } from './engine';
|
|
19
|
-
import type { EngineResult, PipelineEvent, RunPipelineOptions } from './engine';
|
|
20
|
-
import type { PipelineConfig, TaskState } from './types';
|
|
21
|
-
import { generateRunId } from './utils';
|
|
22
|
-
|
|
23
|
-
export type { PipelineEvent, EngineResult };
|
|
24
|
-
|
|
25
|
-
export type PipelineRunnerStatus = 'idle' | 'running' | 'done' | 'aborted';
|
|
26
|
-
|
|
27
|
-
export class PipelineRunner {
|
|
28
|
-
/**
|
|
29
|
-
* Stable ID assigned before start() — safe to use as a Map key in the sidecar
|
|
30
|
-
* before the engine-assigned runId becomes available.
|
|
31
|
-
*/
|
|
32
|
-
readonly instanceId: string;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The runId generated by the engine. Available after the first 'pipeline_start'
|
|
36
|
-
* event fires (i.e. effectively immediately after start() is called).
|
|
37
|
-
* null until then.
|
|
38
|
-
*/
|
|
39
|
-
private _runId: string | null = null;
|
|
40
|
-
private _status: PipelineRunnerStatus = 'idle';
|
|
41
|
-
private _result: Promise<EngineResult> | null = null;
|
|
42
|
-
private _abortController = new AbortController();
|
|
43
|
-
private _handlers = new Set<(event: PipelineEvent) => void>();
|
|
44
|
-
private _states: ReadonlyMap<string, TaskState> | null = null;
|
|
45
|
-
private _statesMirror = new Map<string, TaskState>();
|
|
46
|
-
|
|
47
|
-
constructor(
|
|
48
|
-
private readonly config: PipelineConfig,
|
|
49
|
-
private readonly workDir: string,
|
|
50
|
-
private readonly opts: Omit<RunPipelineOptions, 'signal' | 'onEvent'> = {},
|
|
51
|
-
) {
|
|
52
|
-
this.instanceId = generateRunId();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
get runId(): string | null {
|
|
56
|
-
return this._runId;
|
|
57
|
-
}
|
|
58
|
-
get status(): PipelineRunnerStatus {
|
|
59
|
-
return this._status;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Start the pipeline. Calling start() more than once returns the same Promise.
|
|
64
|
-
*/
|
|
65
|
-
start(): Promise<EngineResult> {
|
|
66
|
-
if (this._result) return this._result;
|
|
67
|
-
|
|
68
|
-
// Guard: if abort() was called before start(), the signal is already
|
|
69
|
-
// aborted. Create a fresh controller so the pipeline doesn't terminate
|
|
70
|
-
// immediately. If users truly want pre-abort semantics, they call
|
|
71
|
-
// abort() after start().
|
|
72
|
-
if (this._abortController.signal.aborted) {
|
|
73
|
-
this._abortController = new AbortController();
|
|
74
|
-
this._status = 'idle';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
this._status = 'running';
|
|
78
|
-
this._result = runPipeline(this.config, this.workDir, {
|
|
79
|
-
...this.opts,
|
|
80
|
-
signal: this._abortController.signal,
|
|
81
|
-
onEvent: (event) => {
|
|
82
|
-
if (event.type === 'pipeline_start') {
|
|
83
|
-
this._runId = event.runId;
|
|
84
|
-
// Initialize the live mirror with the full initial state snapshot
|
|
85
|
-
for (const [id, state] of event.states) {
|
|
86
|
-
this._statesMirror.set(id, { ...state });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (event.type === 'task_status_change') {
|
|
90
|
-
// Keep the mirror up to date so getStates() works during the run
|
|
91
|
-
this._statesMirror.set(event.taskId, event.state);
|
|
92
|
-
}
|
|
93
|
-
if (event.type === 'pipeline_end') {
|
|
94
|
-
this._status = this._abortController.signal.aborted ? 'aborted' : 'done';
|
|
95
|
-
}
|
|
96
|
-
for (const h of this._handlers) h(event);
|
|
97
|
-
},
|
|
98
|
-
})
|
|
99
|
-
.then((result) => {
|
|
100
|
-
this._states = result.states;
|
|
101
|
-
if (this._status === 'running') this._status = 'done';
|
|
102
|
-
return result;
|
|
103
|
-
})
|
|
104
|
-
.catch((err) => {
|
|
105
|
-
this._status = 'aborted';
|
|
106
|
-
throw err;
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
return this._result;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Cancel the running pipeline. Safe to call multiple times or before start().
|
|
114
|
-
*/
|
|
115
|
-
abort(reason?: string): void {
|
|
116
|
-
this._status = 'aborted';
|
|
117
|
-
this._abortController.abort(reason);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Live snapshot of task states. Available from the first pipeline_start event onward
|
|
122
|
-
* (i.e. as soon as start() is called) and remains accessible after the run completes.
|
|
123
|
-
* Returns null only if the pipeline has never started.
|
|
124
|
-
*/
|
|
125
|
-
getStates(): ReadonlyMap<string, TaskState> | null {
|
|
126
|
-
if (this._states) return snapshotStates(this._states);
|
|
127
|
-
if (this._statesMirror.size > 0) return snapshotStates(this._statesMirror);
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Subscribe to pipeline/task events. Returns an unsubscribe function.
|
|
133
|
-
* Events are emitted synchronously in the engine's event loop, so keep
|
|
134
|
-
* handlers non-blocking (e.g. queue to IPC, do not await inside).
|
|
135
|
-
*/
|
|
136
|
-
subscribe(handler: (event: PipelineEvent) => void): () => void {
|
|
137
|
-
this._handlers.add(handler);
|
|
138
|
-
return () => this._handlers.delete(handler);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Deep-copy a states map so callers cannot mutate SDK internals. */
|
|
143
|
-
function snapshotStates(src: ReadonlyMap<string, TaskState>): ReadonlyMap<string, TaskState> {
|
|
144
|
-
const copy = new Map<string, TaskState>();
|
|
145
|
-
for (const [id, s] of src) {
|
|
146
|
-
copy.set(id, {
|
|
147
|
-
config: { ...s.config },
|
|
148
|
-
trackConfig: { ...s.trackConfig },
|
|
149
|
-
status: s.status,
|
|
150
|
-
result: s.result ? { ...s.result } : null,
|
|
151
|
-
startedAt: s.startedAt,
|
|
152
|
-
finishedAt: s.finishedAt,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
return copy;
|
|
156
|
-
}
|
|
1
|
+
// ═══ PipelineRunner ═══
|
|
2
|
+
//
|
|
3
|
+
// Wraps runPipeline in a lifecycle object suited for multi-pipeline management
|
|
4
|
+
// in sidecar / Tauri IPC scenarios. Each instance controls one pipeline run.
|
|
5
|
+
//
|
|
6
|
+
// Typical sidecar usage:
|
|
7
|
+
//
|
|
8
|
+
// const runners = new Map<string, PipelineRunner>();
|
|
9
|
+
//
|
|
10
|
+
// const runner = new PipelineRunner(config, workDir);
|
|
11
|
+
// runner.subscribe(event => ipcEmit('pipeline_event', event));
|
|
12
|
+
// runner.start();
|
|
13
|
+
// runners.set(runner.instanceId, runner);
|
|
14
|
+
//
|
|
15
|
+
// // Later, from IPC:
|
|
16
|
+
// runners.get(id)?.abort();
|
|
17
|
+
|
|
18
|
+
import { runPipeline } from './engine';
|
|
19
|
+
import type { EngineResult, PipelineEvent, RunPipelineOptions } from './engine';
|
|
20
|
+
import type { PipelineConfig, TaskState } from './types';
|
|
21
|
+
import { generateRunId } from './utils';
|
|
22
|
+
|
|
23
|
+
export type { PipelineEvent, EngineResult };
|
|
24
|
+
|
|
25
|
+
export type PipelineRunnerStatus = 'idle' | 'running' | 'done' | 'aborted';
|
|
26
|
+
|
|
27
|
+
export class PipelineRunner {
|
|
28
|
+
/**
|
|
29
|
+
* Stable ID assigned before start() — safe to use as a Map key in the sidecar
|
|
30
|
+
* before the engine-assigned runId becomes available.
|
|
31
|
+
*/
|
|
32
|
+
readonly instanceId: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The runId generated by the engine. Available after the first 'pipeline_start'
|
|
36
|
+
* event fires (i.e. effectively immediately after start() is called).
|
|
37
|
+
* null until then.
|
|
38
|
+
*/
|
|
39
|
+
private _runId: string | null = null;
|
|
40
|
+
private _status: PipelineRunnerStatus = 'idle';
|
|
41
|
+
private _result: Promise<EngineResult> | null = null;
|
|
42
|
+
private _abortController = new AbortController();
|
|
43
|
+
private _handlers = new Set<(event: PipelineEvent) => void>();
|
|
44
|
+
private _states: ReadonlyMap<string, TaskState> | null = null;
|
|
45
|
+
private _statesMirror = new Map<string, TaskState>();
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly config: PipelineConfig,
|
|
49
|
+
private readonly workDir: string,
|
|
50
|
+
private readonly opts: Omit<RunPipelineOptions, 'signal' | 'onEvent'> = {},
|
|
51
|
+
) {
|
|
52
|
+
this.instanceId = generateRunId();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get runId(): string | null {
|
|
56
|
+
return this._runId;
|
|
57
|
+
}
|
|
58
|
+
get status(): PipelineRunnerStatus {
|
|
59
|
+
return this._status;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Start the pipeline. Calling start() more than once returns the same Promise.
|
|
64
|
+
*/
|
|
65
|
+
start(): Promise<EngineResult> {
|
|
66
|
+
if (this._result) return this._result;
|
|
67
|
+
|
|
68
|
+
// Guard: if abort() was called before start(), the signal is already
|
|
69
|
+
// aborted. Create a fresh controller so the pipeline doesn't terminate
|
|
70
|
+
// immediately. If users truly want pre-abort semantics, they call
|
|
71
|
+
// abort() after start().
|
|
72
|
+
if (this._abortController.signal.aborted) {
|
|
73
|
+
this._abortController = new AbortController();
|
|
74
|
+
this._status = 'idle';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this._status = 'running';
|
|
78
|
+
this._result = runPipeline(this.config, this.workDir, {
|
|
79
|
+
...this.opts,
|
|
80
|
+
signal: this._abortController.signal,
|
|
81
|
+
onEvent: (event) => {
|
|
82
|
+
if (event.type === 'pipeline_start') {
|
|
83
|
+
this._runId = event.runId;
|
|
84
|
+
// Initialize the live mirror with the full initial state snapshot
|
|
85
|
+
for (const [id, state] of event.states) {
|
|
86
|
+
this._statesMirror.set(id, { ...state });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (event.type === 'task_status_change') {
|
|
90
|
+
// Keep the mirror up to date so getStates() works during the run
|
|
91
|
+
this._statesMirror.set(event.taskId, event.state);
|
|
92
|
+
}
|
|
93
|
+
if (event.type === 'pipeline_end') {
|
|
94
|
+
this._status = this._abortController.signal.aborted ? 'aborted' : 'done';
|
|
95
|
+
}
|
|
96
|
+
for (const h of this._handlers) h(event);
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
.then((result) => {
|
|
100
|
+
this._states = result.states;
|
|
101
|
+
if (this._status === 'running') this._status = 'done';
|
|
102
|
+
return result;
|
|
103
|
+
})
|
|
104
|
+
.catch((err) => {
|
|
105
|
+
this._status = 'aborted';
|
|
106
|
+
throw err;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return this._result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Cancel the running pipeline. Safe to call multiple times or before start().
|
|
114
|
+
*/
|
|
115
|
+
abort(reason?: string): void {
|
|
116
|
+
this._status = 'aborted';
|
|
117
|
+
this._abortController.abort(reason);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Live snapshot of task states. Available from the first pipeline_start event onward
|
|
122
|
+
* (i.e. as soon as start() is called) and remains accessible after the run completes.
|
|
123
|
+
* Returns null only if the pipeline has never started.
|
|
124
|
+
*/
|
|
125
|
+
getStates(): ReadonlyMap<string, TaskState> | null {
|
|
126
|
+
if (this._states) return snapshotStates(this._states);
|
|
127
|
+
if (this._statesMirror.size > 0) return snapshotStates(this._statesMirror);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Subscribe to pipeline/task events. Returns an unsubscribe function.
|
|
133
|
+
* Events are emitted synchronously in the engine's event loop, so keep
|
|
134
|
+
* handlers non-blocking (e.g. queue to IPC, do not await inside).
|
|
135
|
+
*/
|
|
136
|
+
subscribe(handler: (event: PipelineEvent) => void): () => void {
|
|
137
|
+
this._handlers.add(handler);
|
|
138
|
+
return () => this._handlers.delete(handler);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Deep-copy a states map so callers cannot mutate SDK internals. */
|
|
143
|
+
function snapshotStates(src: ReadonlyMap<string, TaskState>): ReadonlyMap<string, TaskState> {
|
|
144
|
+
const copy = new Map<string, TaskState>();
|
|
145
|
+
for (const [id, s] of src) {
|
|
146
|
+
copy.set(id, {
|
|
147
|
+
config: { ...s.config },
|
|
148
|
+
trackConfig: { ...s.trackConfig },
|
|
149
|
+
status: s.status,
|
|
150
|
+
result: s.result ? { ...s.result } : null,
|
|
151
|
+
startedAt: s.startedAt,
|
|
152
|
+
finishedAt: s.finishedAt,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return copy;
|
|
156
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { PromptDocument, PromptContextBlock } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build a fresh `PromptDocument` from a raw task string.
|
|
5
|
+
* Middlewares receive this from the engine and push context blocks onto
|
|
6
|
+
* `contexts`. `task` is the user's original prompt and should not be
|
|
7
|
+
* rewritten by middlewares (translation middlewares are the rare exception).
|
|
8
|
+
*/
|
|
9
|
+
export function promptDocumentFromString(task: string): PromptDocument {
|
|
10
|
+
return { contexts: [], task };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Serialize a `PromptDocument` to the default string form consumed by
|
|
15
|
+
* drivers that read `task.prompt` instead of `ctx.promptDoc`.
|
|
16
|
+
*
|
|
17
|
+
* Format:
|
|
18
|
+
*
|
|
19
|
+
* [<label1>]
|
|
20
|
+
* <content1>
|
|
21
|
+
*
|
|
22
|
+
* [<label2>]
|
|
23
|
+
* <content2>
|
|
24
|
+
*
|
|
25
|
+
* <task>
|
|
26
|
+
*
|
|
27
|
+
* Each context block is separated from the next (and from `task`) by a
|
|
28
|
+
* single blank line. No implicit `[Task]` header is emitted — that framing
|
|
29
|
+
* is the driver's responsibility (e.g. opencode's `agent_profile` wrapping).
|
|
30
|
+
* Emitting one here would compose incorrectly with any driver that also
|
|
31
|
+
* adds a `[Task]` header, producing a double header that some models
|
|
32
|
+
* (observed with `opencode/big-pickle`) misread as a cut-off message.
|
|
33
|
+
*/
|
|
34
|
+
export function serializePromptDocument(doc: PromptDocument): string {
|
|
35
|
+
if (doc.contexts.length === 0) return doc.task;
|
|
36
|
+
const blocks = doc.contexts.map((c) => `[${c.label}]\n${c.content}`);
|
|
37
|
+
return `${blocks.join('\n\n')}\n\n${doc.task}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Helper for middlewares: return a new document with the given block
|
|
42
|
+
* appended to `contexts`, preserving immutability of `doc`.
|
|
43
|
+
*/
|
|
44
|
+
export function appendContext(
|
|
45
|
+
doc: PromptDocument,
|
|
46
|
+
block: PromptContextBlock,
|
|
47
|
+
): PromptDocument {
|
|
48
|
+
return { contexts: [...doc.contexts, block], task: doc.task };
|
|
49
|
+
}
|