@labacacia/nps-sdk 1.0.0-alpha.1 → 1.0.0-alpha.2

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 (46) hide show
  1. package/.npmrc.publish +1 -0
  2. package/CHANGELOG.cn.md +39 -0
  3. package/CHANGELOG.md +39 -0
  4. package/CONTRIBUTING.cn.md +35 -0
  5. package/CONTRIBUTING.md +2 -0
  6. package/README.cn.md +155 -0
  7. package/README.md +5 -3
  8. package/dist/core/frames.d.ts +1 -0
  9. package/dist/core/frames.d.ts.map +1 -1
  10. package/dist/core/frames.js +1 -0
  11. package/dist/core/frames.js.map +1 -1
  12. package/dist/core/index.d.ts +6 -4
  13. package/dist/core/index.d.ts.map +1 -1
  14. package/dist/core/index.js +17 -5
  15. package/dist/core/index.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/ncp/frames.d.ts +18 -0
  21. package/dist/ncp/frames.d.ts.map +1 -1
  22. package/dist/ncp/frames.js +45 -0
  23. package/dist/ncp/frames.js.map +1 -1
  24. package/dist/ncp/registry.d.ts.map +1 -1
  25. package/dist/ncp/registry.js +2 -1
  26. package/dist/ncp/registry.js.map +1 -1
  27. package/doc/nps-sdk.core.cn.md +321 -0
  28. package/doc/nps-sdk.core.md +326 -0
  29. package/doc/nps-sdk.ncp.cn.md +270 -0
  30. package/doc/nps-sdk.ncp.md +276 -0
  31. package/doc/nps-sdk.ndp.cn.md +267 -0
  32. package/doc/nps-sdk.ndp.md +273 -0
  33. package/doc/nps-sdk.nip.cn.md +235 -0
  34. package/doc/nps-sdk.nip.md +242 -0
  35. package/doc/nps-sdk.nop.cn.md +329 -0
  36. package/doc/nps-sdk.nop.md +332 -0
  37. package/doc/nps-sdk.nwp.cn.md +217 -0
  38. package/doc/nps-sdk.nwp.md +224 -0
  39. package/doc/overview.cn.md +149 -0
  40. package/doc/overview.md +153 -0
  41. package/package.json +21 -4
  42. package/src/core/frames.ts +1 -0
  43. package/src/core/index.ts +37 -5
  44. package/src/index.ts +1 -1
  45. package/src/ncp/frames.ts +52 -0
  46. package/src/ncp/registry.ts +2 -1
@@ -0,0 +1,329 @@
1
+ [English Version](./nps-sdk.nop.md) | 中文版
2
+
3
+ # `@labacacia/nps-sdk/nop` — 类与方法参考
4
+
5
+ > 规范:[NPS-5 NOP v0.3](https://github.com/labacacia/NPS-Release/blob/main/spec/NPS-5-NOP.md)
6
+
7
+ NOP 是编排层 —— 提交一个委托子任务 DAG、等待完成、流式拉回结果。
8
+ 本模块提供四个 NOP 帧(0x40–0x43)、任务模型(`TaskDag`、`DagNode`、
9
+ `DagEdge`、`RetryPolicy`、`TaskContext`),以及异步 `NopClient` + `NopTaskStatus`。
10
+
11
+ ---
12
+
13
+ ## 目录
14
+
15
+ - [枚举与常量](#枚举与常量)
16
+ - [`RetryPolicy`](#retrypolicy)
17
+ - [`TaskContext`](#taskcontext)
18
+ - [`DagNode` / `DagEdge` / `TaskDag`](#dagnode--dagedge--taskdag)
19
+ - [`TaskFrame` (0x40)](#taskframe-0x40)
20
+ - [`DelegateFrame` (0x41)](#delegateframe-0x41)
21
+ - [`SyncFrame` (0x42)](#syncframe-0x42)
22
+ - [`AlignStreamFrame` (0x43)](#alignstreamframe-0x43)
23
+ - [`NopClient`](#nopclient)
24
+ - [`NopTaskStatus`](#noptaskstatus)
25
+
26
+ ---
27
+
28
+ ## 枚举与常量
29
+
30
+ ```typescript
31
+ enum TaskState {
32
+ PENDING = "pending",
33
+ PREFLIGHT = "preflight",
34
+ RUNNING = "running",
35
+ WAITING_SYNC = "waiting_sync",
36
+ COMPLETED = "completed",
37
+ FAILED = "failed",
38
+ CANCELLED = "cancelled",
39
+ SKIPPED = "skipped",
40
+ }
41
+
42
+ enum TaskPriority { LOW = "low", NORMAL = "normal", HIGH = "high" }
43
+ enum BackoffStrategy { FIXED = "fixed", LINEAR = "linear", EXPONENTIAL = "exponential" }
44
+ enum AggregateStrategy { MERGE = "merge", FIRST = "first", FASTEST_K = "fastest_k", ALL = "all" }
45
+ ```
46
+
47
+ 终态为 `COMPLETED`、`FAILED`、`CANCELLED`。`NopTaskStatus.isTerminal`
48
+ 使用此集合。
49
+
50
+ ---
51
+
52
+ ## `RetryPolicy`
53
+
54
+ ```typescript
55
+ interface RetryPolicy {
56
+ maxRetries: number;
57
+ backoff: BackoffStrategy;
58
+ baseDelayMs?: number; // 默认 1 000
59
+ maxDelayMs?: number; // 默认 30 000
60
+ }
61
+
62
+ function computeDelayMs(policy: RetryPolicy, attempt: number): number;
63
+ ```
64
+
65
+ `computeDelayMs` 计算 `attempt`(从 0 起)的限幅延迟:
66
+
67
+ | Backoff | 公式 |
68
+ |---------------|------|
69
+ | `FIXED` | `baseDelayMs` |
70
+ | `LINEAR` | `baseDelayMs * (attempt + 1)` |
71
+ | `EXPONENTIAL` | `baseDelayMs * 2**attempt` |
72
+
73
+ 结果上限为 `maxDelayMs`。
74
+
75
+ ---
76
+
77
+ ## `TaskContext`
78
+
79
+ ```typescript
80
+ interface TaskContext {
81
+ sessionKey?: string;
82
+ requesterNid?: string;
83
+ traceId?: string; // OpenTelemetry 风格的 trace id
84
+ }
85
+ ```
86
+
87
+ ---
88
+
89
+ ## `DagNode` / `DagEdge` / `TaskDag`
90
+
91
+ ```typescript
92
+ interface DagNode {
93
+ id: string;
94
+ action: string;
95
+ agent: string; // 目标 NID
96
+ inputFrom?: readonly string[]; // 上游节点 id
97
+ inputMapping?: Record<string, string>; // 可选 JSONPath 改写
98
+ timeoutMs?: number;
99
+ retryPolicy?: RetryPolicy;
100
+ condition?: string; // JSONPath 风格守卫,如 "$.classify.score > 0.7"
101
+ minRequired?: number; // K-of-N fan-in
102
+ }
103
+
104
+ interface DagEdge {
105
+ from: string;
106
+ to: string;
107
+ }
108
+
109
+ interface TaskDag {
110
+ nodes: readonly DagNode[];
111
+ edges: readonly DagEdge[];
112
+ }
113
+ ```
114
+
115
+ 按规范:每个 DAG 最多 32 节点、委托链最多 3 层、超时上限
116
+ 3 600 000 ms(1 小时)。超过上述任一限制将被编排器拒绝(NPS-5 §8.2)。
117
+
118
+ ---
119
+
120
+ ## `TaskFrame` (0x40)
121
+
122
+ 提交 DAG 供执行。
123
+
124
+ ```typescript
125
+ class TaskFrame {
126
+ readonly frameType: FrameType.TASK;
127
+ readonly preferredTier: EncodingTier.MSGPACK;
128
+
129
+ constructor(
130
+ public readonly taskId: string,
131
+ public readonly dag: TaskDag,
132
+ public readonly timeoutMs?: number,
133
+ public readonly callbackUrl?: string, // 编排器做 SSRF 校验
134
+ public readonly context?: TaskContext,
135
+ public readonly priority?: TaskPriority,
136
+ public readonly depth?: number,
137
+ );
138
+
139
+ toDict(): Record<string, unknown>;
140
+ static fromDict(data: Record<string, unknown>): TaskFrame;
141
+ }
142
+ ```
143
+
144
+ ---
145
+
146
+ ## `DelegateFrame` (0x41)
147
+
148
+ 编排器向每个 agent 发出的逐节点调用。
149
+
150
+ ```typescript
151
+ class DelegateFrame {
152
+ readonly frameType: FrameType.DELEGATE;
153
+ readonly preferredTier: EncodingTier.MSGPACK;
154
+
155
+ constructor(
156
+ public readonly taskId: string,
157
+ public readonly subtaskId: string,
158
+ public readonly action: string,
159
+ public readonly agentNid: string,
160
+ public readonly inputs?: Record<string, unknown>,
161
+ public readonly params?: Record<string, unknown>,
162
+ public readonly idempotencyKey?: string,
163
+ );
164
+
165
+ toDict(): Record<string, unknown>;
166
+ static fromDict(data: Record<string, unknown>): DelegateFrame;
167
+ }
168
+ ```
169
+
170
+ ---
171
+
172
+ ## `SyncFrame` (0x42)
173
+
174
+ Fan-in 屏障 —— 等待 K-of-N 上游子任务。
175
+
176
+ ```typescript
177
+ class SyncFrame {
178
+ readonly frameType: FrameType.SYNC;
179
+ readonly preferredTier: EncodingTier.MSGPACK;
180
+
181
+ constructor(
182
+ public readonly taskId: string,
183
+ public readonly syncId: string,
184
+ public readonly waitFor: readonly string[],
185
+ public readonly minRequired: number = 0, // 0 = waitFor 全部
186
+ public readonly aggregate: AggregateStrategy | string = "merge",
187
+ public readonly timeoutMs?: number,
188
+ );
189
+
190
+ toDict(): Record<string, unknown>;
191
+ static fromDict(data: Record<string, unknown>): SyncFrame;
192
+ }
193
+ ```
194
+
195
+ `minRequired` 语义:
196
+
197
+ | 值 | 含义 |
198
+ |-------|------|
199
+ | `0` | 等待 `waitFor` 中所有项(严格 fan-in)。 |
200
+ | `K` | 只要 K 个上游子任务完成即继续。 |
201
+
202
+ ---
203
+
204
+ ## `AlignStreamFrame` (0x43)
205
+
206
+ 委托子任务的流式进度 / 部分结果帧。
207
+
208
+ ```typescript
209
+ interface StreamError {
210
+ errorCode: string;
211
+ message?: string;
212
+ }
213
+
214
+ class AlignStreamFrame {
215
+ readonly frameType: FrameType.ALIGN_STREAM;
216
+ readonly preferredTier: EncodingTier.MSGPACK;
217
+
218
+ constructor(
219
+ public readonly streamId: string,
220
+ public readonly taskId: string,
221
+ public readonly subtaskId: string,
222
+ public readonly seq: number,
223
+ public readonly isFinal: boolean,
224
+ public readonly senderNid: string,
225
+ public readonly data?: Record<string, unknown>,
226
+ public readonly error?: StreamError,
227
+ public readonly windowSize?: number,
228
+ );
229
+
230
+ toDict(): Record<string, unknown>;
231
+ static fromDict(data: Record<string, unknown>): AlignStreamFrame;
232
+ }
233
+ ```
234
+
235
+ `AlignStreamFrame` 替代已弃用的 `AlignFrame (0x05)` —— 它携带
236
+ 任务上下文(`taskId` + `subtaskId`)且绑定到特定 `senderNid`。
237
+
238
+ ---
239
+
240
+ ## `NopClient`
241
+
242
+ NOP 编排器的异步 HTTP 客户端。
243
+
244
+ ```typescript
245
+ class NopClient {
246
+ constructor(
247
+ baseUrl: string,
248
+ options?: {
249
+ defaultTier?: EncodingTier; // 默认 MSGPACK
250
+ registry?: FrameRegistry; // 默认 NCP + NOP 帧
251
+ },
252
+ );
253
+
254
+ async submit(frame: TaskFrame): Promise<string>; // 返回 taskId
255
+ async getStatus(taskId: string): Promise<NopTaskStatus>;
256
+ async cancel(taskId: string): Promise<void>;
257
+ async wait(
258
+ taskId: string,
259
+ options?: { pollIntervalMs?: number; timeoutMs?: number },
260
+ ): Promise<NopTaskStatus>;
261
+ }
262
+ ```
263
+
264
+ ### HTTP 路由
265
+
266
+ | 方法 | 路径 |
267
+ |-------------|----------------------------|
268
+ | `submit` | `POST /task` |
269
+ | `getStatus` | `GET /task/{taskId}` |
270
+ | `cancel` | `POST /task/{taskId}/cancel` |
271
+ | `wait` | 轮询 `getStatus` 直至终态或超时 |
272
+
273
+ `wait` 默认:`pollIntervalMs = 1000`、`timeoutMs = 30 000`。若截止时间
274
+ 到达仍未抵达终态则抛 `Error`。
275
+
276
+ ---
277
+
278
+ ## `NopTaskStatus`
279
+
280
+ 编排器 JSON 响应的薄视图。
281
+
282
+ ```typescript
283
+ class NopTaskStatus {
284
+ readonly taskId: string;
285
+ readonly state: TaskState;
286
+ readonly isTerminal: boolean; // COMPLETED | FAILED | CANCELLED
287
+ readonly aggregatedResult: unknown;
288
+ readonly errorCode?: string;
289
+ readonly errorMessage?: string;
290
+ readonly nodeResults: Record<string, unknown>;
291
+ readonly raw: Record<string, unknown>;
292
+ }
293
+ ```
294
+
295
+ 若需要 `NopTaskStatus` 上没有一等支持的编排器专属字段,`raw` 提供
296
+ 未经处理的原始 payload。
297
+
298
+ ---
299
+
300
+ ## 端到端示例
301
+
302
+ ```typescript
303
+ import {
304
+ NopClient, TaskFrame,
305
+ type TaskDag, BackoffStrategy,
306
+ } from "@labacacia/nps-sdk/nop";
307
+
308
+ const dag: TaskDag = {
309
+ nodes: [
310
+ { id: "fetch", action: "fetch", agent: "urn:nps:node:ingest.example.com:http" },
311
+ { id: "classify", action: "classify", agent: "urn:nps:node:ml.example.com:classifier",
312
+ inputFrom: ["fetch"],
313
+ retryPolicy: { maxRetries: 3, backoff: BackoffStrategy.EXPONENTIAL, baseDelayMs: 500 } },
314
+ { id: "route", action: "route", agent: "urn:nps:node:ml.example.com:router",
315
+ inputFrom: ["classify"],
316
+ condition: "$.classify.score > 0.7" },
317
+ ],
318
+ edges: [
319
+ { from: "fetch", to: "classify" },
320
+ { from: "classify", to: "route" },
321
+ ],
322
+ };
323
+
324
+ const nop = new NopClient("http://orchestrator.example.com:17433");
325
+ const taskId = await nop.submit(new TaskFrame("job-42", dag, 60_000));
326
+ const status = await nop.wait(taskId, { pollIntervalMs: 500, timeoutMs: 60_000 });
327
+
328
+ console.log(status.state, status.aggregatedResult);
329
+ ```
@@ -0,0 +1,332 @@
1
+ English | [中文版](./nps-sdk.nop.cn.md)
2
+
3
+ # `@labacacia/nps-sdk/nop` — Class and Method Reference
4
+
5
+ > Spec: [NPS-5 NOP v0.3](https://github.com/labacacia/NPS-Release/blob/main/spec/NPS-5-NOP.md)
6
+
7
+ NOP is the orchestration layer — submit a DAG of delegated subtasks, wait
8
+ for completion, stream results back. This module ships the four NOP
9
+ frames (0x40–0x43), the task model (`TaskDag`, `DagNode`, `DagEdge`,
10
+ `RetryPolicy`, `TaskContext`), and the async `NopClient` + `NopTaskStatus`.
11
+
12
+ ---
13
+
14
+ ## Table of contents
15
+
16
+ - [Enums & constants](#enums--constants)
17
+ - [`RetryPolicy`](#retrypolicy)
18
+ - [`TaskContext`](#taskcontext)
19
+ - [`DagNode` / `DagEdge` / `TaskDag`](#dagnode--dagedge--taskdag)
20
+ - [`TaskFrame` (0x40)](#taskframe-0x40)
21
+ - [`DelegateFrame` (0x41)](#delegateframe-0x41)
22
+ - [`SyncFrame` (0x42)](#syncframe-0x42)
23
+ - [`AlignStreamFrame` (0x43)](#alignstreamframe-0x43)
24
+ - [`NopClient`](#nopclient)
25
+ - [`NopTaskStatus`](#noptaskstatus)
26
+
27
+ ---
28
+
29
+ ## Enums & constants
30
+
31
+ ```typescript
32
+ enum TaskState {
33
+ PENDING = "pending",
34
+ PREFLIGHT = "preflight",
35
+ RUNNING = "running",
36
+ WAITING_SYNC = "waiting_sync",
37
+ COMPLETED = "completed",
38
+ FAILED = "failed",
39
+ CANCELLED = "cancelled",
40
+ SKIPPED = "skipped",
41
+ }
42
+
43
+ enum TaskPriority { LOW = "low", NORMAL = "normal", HIGH = "high" }
44
+ enum BackoffStrategy { FIXED = "fixed", LINEAR = "linear", EXPONENTIAL = "exponential" }
45
+ enum AggregateStrategy { MERGE = "merge", FIRST = "first", FASTEST_K = "fastest_k", ALL = "all" }
46
+ ```
47
+
48
+ Terminal states are `COMPLETED`, `FAILED`, `CANCELLED`. `NopTaskStatus.isTerminal`
49
+ uses this set.
50
+
51
+ ---
52
+
53
+ ## `RetryPolicy`
54
+
55
+ ```typescript
56
+ interface RetryPolicy {
57
+ maxRetries: number;
58
+ backoff: BackoffStrategy;
59
+ baseDelayMs?: number; // default 1 000
60
+ maxDelayMs?: number; // default 30 000
61
+ }
62
+
63
+ function computeDelayMs(policy: RetryPolicy, attempt: number): number;
64
+ ```
65
+
66
+ `computeDelayMs` computes the clamped delay for `attempt` (0-indexed):
67
+
68
+ | Backoff | Formula |
69
+ |---------------|---------|
70
+ | `FIXED` | `baseDelayMs` |
71
+ | `LINEAR` | `baseDelayMs * (attempt + 1)` |
72
+ | `EXPONENTIAL` | `baseDelayMs * 2**attempt` |
73
+
74
+ The result is capped at `maxDelayMs`.
75
+
76
+ ---
77
+
78
+ ## `TaskContext`
79
+
80
+ ```typescript
81
+ interface TaskContext {
82
+ sessionKey?: string;
83
+ requesterNid?: string;
84
+ traceId?: string; // OpenTelemetry-shaped trace id
85
+ }
86
+ ```
87
+
88
+ ---
89
+
90
+ ## `DagNode` / `DagEdge` / `TaskDag`
91
+
92
+ ```typescript
93
+ interface DagNode {
94
+ id: string;
95
+ action: string;
96
+ agent: string; // target NID
97
+ inputFrom?: readonly string[]; // upstream node ids
98
+ inputMapping?: Record<string, string>; // optional JSONPath rewrites
99
+ timeoutMs?: number;
100
+ retryPolicy?: RetryPolicy;
101
+ condition?: string; // JSONPath-style guard, e.g. "$.classify.score > 0.7"
102
+ minRequired?: number; // K-of-N fan-in
103
+ }
104
+
105
+ interface DagEdge {
106
+ from: string;
107
+ to: string;
108
+ }
109
+
110
+ interface TaskDag {
111
+ nodes: readonly DagNode[];
112
+ edges: readonly DagEdge[];
113
+ }
114
+ ```
115
+
116
+ Per the spec: max 32 nodes per DAG, max 3 levels of delegate chain, max
117
+ timeout 3 600 000 ms (1 h). Exceeding any of those limits is rejected by
118
+ the orchestrator (NPS-5 §8.2).
119
+
120
+ ---
121
+
122
+ ## `TaskFrame` (0x40)
123
+
124
+ Submit a DAG for execution.
125
+
126
+ ```typescript
127
+ class TaskFrame {
128
+ readonly frameType: FrameType.TASK;
129
+ readonly preferredTier: EncodingTier.MSGPACK;
130
+
131
+ constructor(
132
+ public readonly taskId: string,
133
+ public readonly dag: TaskDag,
134
+ public readonly timeoutMs?: number,
135
+ public readonly callbackUrl?: string, // SSRF-validated by the orchestrator
136
+ public readonly context?: TaskContext,
137
+ public readonly priority?: TaskPriority,
138
+ public readonly depth?: number,
139
+ );
140
+
141
+ toDict(): Record<string, unknown>;
142
+ static fromDict(data: Record<string, unknown>): TaskFrame;
143
+ }
144
+ ```
145
+
146
+ ---
147
+
148
+ ## `DelegateFrame` (0x41)
149
+
150
+ Per-node invocation emitted by the orchestrator to each agent.
151
+
152
+ ```typescript
153
+ class DelegateFrame {
154
+ readonly frameType: FrameType.DELEGATE;
155
+ readonly preferredTier: EncodingTier.MSGPACK;
156
+
157
+ constructor(
158
+ public readonly taskId: string,
159
+ public readonly subtaskId: string,
160
+ public readonly action: string,
161
+ public readonly agentNid: string,
162
+ public readonly inputs?: Record<string, unknown>,
163
+ public readonly params?: Record<string, unknown>,
164
+ public readonly idempotencyKey?: string,
165
+ );
166
+
167
+ toDict(): Record<string, unknown>;
168
+ static fromDict(data: Record<string, unknown>): DelegateFrame;
169
+ }
170
+ ```
171
+
172
+ ---
173
+
174
+ ## `SyncFrame` (0x42)
175
+
176
+ Fan-in barrier — waits for K-of-N upstream subtasks.
177
+
178
+ ```typescript
179
+ class SyncFrame {
180
+ readonly frameType: FrameType.SYNC;
181
+ readonly preferredTier: EncodingTier.MSGPACK;
182
+
183
+ constructor(
184
+ public readonly taskId: string,
185
+ public readonly syncId: string,
186
+ public readonly waitFor: readonly string[],
187
+ public readonly minRequired: number = 0, // 0 = all of waitFor
188
+ public readonly aggregate: AggregateStrategy | string = "merge",
189
+ public readonly timeoutMs?: number,
190
+ );
191
+
192
+ toDict(): Record<string, unknown>;
193
+ static fromDict(data: Record<string, unknown>): SyncFrame;
194
+ }
195
+ ```
196
+
197
+ `minRequired` semantics:
198
+
199
+ | Value | Meaning |
200
+ |-------|---------|
201
+ | `0` | Wait for all of `waitFor` (strict fan-in). |
202
+ | `K` | Proceed as soon as K upstream subtasks have completed. |
203
+
204
+ ---
205
+
206
+ ## `AlignStreamFrame` (0x43)
207
+
208
+ Streaming progress / partial result frame for a delegated subtask.
209
+
210
+ ```typescript
211
+ interface StreamError {
212
+ errorCode: string;
213
+ message?: string;
214
+ }
215
+
216
+ class AlignStreamFrame {
217
+ readonly frameType: FrameType.ALIGN_STREAM;
218
+ readonly preferredTier: EncodingTier.MSGPACK;
219
+
220
+ constructor(
221
+ public readonly streamId: string,
222
+ public readonly taskId: string,
223
+ public readonly subtaskId: string,
224
+ public readonly seq: number,
225
+ public readonly isFinal: boolean,
226
+ public readonly senderNid: string,
227
+ public readonly data?: Record<string, unknown>,
228
+ public readonly error?: StreamError,
229
+ public readonly windowSize?: number,
230
+ );
231
+
232
+ toDict(): Record<string, unknown>;
233
+ static fromDict(data: Record<string, unknown>): AlignStreamFrame;
234
+ }
235
+ ```
236
+
237
+ `AlignStreamFrame` replaces the deprecated `AlignFrame (0x05)` — it
238
+ carries task context (`taskId` + `subtaskId`) and is bound to a specific
239
+ `senderNid`.
240
+
241
+ ---
242
+
243
+ ## `NopClient`
244
+
245
+ Async HTTP client for an NOP orchestrator.
246
+
247
+ ```typescript
248
+ class NopClient {
249
+ constructor(
250
+ baseUrl: string,
251
+ options?: {
252
+ defaultTier?: EncodingTier; // default MSGPACK
253
+ registry?: FrameRegistry; // default NCP + NOP frames
254
+ },
255
+ );
256
+
257
+ async submit(frame: TaskFrame): Promise<string>; // returns taskId
258
+ async getStatus(taskId: string): Promise<NopTaskStatus>;
259
+ async cancel(taskId: string): Promise<void>;
260
+ async wait(
261
+ taskId: string,
262
+ options?: { pollIntervalMs?: number; timeoutMs?: number },
263
+ ): Promise<NopTaskStatus>;
264
+ }
265
+ ```
266
+
267
+ ### HTTP routes
268
+
269
+ | Method | Path |
270
+ |-------------|----------------------------|
271
+ | `submit` | `POST /task` |
272
+ | `getStatus` | `GET /task/{taskId}` |
273
+ | `cancel` | `POST /task/{taskId}/cancel` |
274
+ | `wait` | polls `getStatus` until terminal or timeout |
275
+
276
+ `wait` defaults: `pollIntervalMs = 1000`, `timeoutMs = 30 000`. It throws
277
+ an `Error` when the deadline expires without reaching a terminal state.
278
+
279
+ ---
280
+
281
+ ## `NopTaskStatus`
282
+
283
+ Thin view over the orchestrator's JSON response.
284
+
285
+ ```typescript
286
+ class NopTaskStatus {
287
+ readonly taskId: string;
288
+ readonly state: TaskState;
289
+ readonly isTerminal: boolean; // COMPLETED | FAILED | CANCELLED
290
+ readonly aggregatedResult: unknown;
291
+ readonly errorCode?: string;
292
+ readonly errorMessage?: string;
293
+ readonly nodeResults: Record<string, unknown>;
294
+ readonly raw: Record<string, unknown>;
295
+ }
296
+ ```
297
+
298
+ `raw` gives you the untouched payload if you need orchestrator-specific
299
+ fields that aren't first-class on `NopTaskStatus`.
300
+
301
+ ---
302
+
303
+ ## End-to-end example
304
+
305
+ ```typescript
306
+ import {
307
+ NopClient, TaskFrame,
308
+ type TaskDag, BackoffStrategy,
309
+ } from "@labacacia/nps-sdk/nop";
310
+
311
+ const dag: TaskDag = {
312
+ nodes: [
313
+ { id: "fetch", action: "fetch", agent: "urn:nps:node:ingest.example.com:http" },
314
+ { id: "classify", action: "classify", agent: "urn:nps:node:ml.example.com:classifier",
315
+ inputFrom: ["fetch"],
316
+ retryPolicy: { maxRetries: 3, backoff: BackoffStrategy.EXPONENTIAL, baseDelayMs: 500 } },
317
+ { id: "route", action: "route", agent: "urn:nps:node:ml.example.com:router",
318
+ inputFrom: ["classify"],
319
+ condition: "$.classify.score > 0.7" },
320
+ ],
321
+ edges: [
322
+ { from: "fetch", to: "classify" },
323
+ { from: "classify", to: "route" },
324
+ ],
325
+ };
326
+
327
+ const nop = new NopClient("http://orchestrator.example.com:17433");
328
+ const taskId = await nop.submit(new TaskFrame("job-42", dag, 60_000));
329
+ const status = await nop.wait(taskId, { pollIntervalMs: 500, timeoutMs: 60_000 });
330
+
331
+ console.log(status.state, status.aggregatedResult);
332
+ ```