@hasna-internal/kai-workflow 0.1.1-rc.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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +61 -0
- package/README.zh.md +61 -0
- package/lib/index.js +92 -0
- package/lib/invariant.js +100 -0
- package/lib/types/index.d.ts +128 -0
- package/lib/types/index.js +80 -0
- package/lib/types/invariant.d.ts +13 -0
- package/lib/types/invariant.js +118 -0
- package/lib/types/runtime-types.d.ts +45 -0
- package/lib/types/runtime-types.js +9 -0
- package/lib/types/types.d.ts +118 -0
- package/lib/types/types.js +16 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/workflow/workflow/README.md
|
|
5
|
+
README.md: cc2c24f62512273ea1657542d0d904b0b870e236
|
|
6
|
+
README.zh.md: 0b02cea896b3cb64559ce110c0da2478728f513c
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @hasna-internal/kai-workflow
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
The workflow seam (`ctx.workflowEngine`) executes a model-written orchestration script that can fan out subagents. The seam defines the script, run, result, error, and event contracts; an engine decides how to isolate and execute the script.
|
|
6
|
+
|
|
7
|
+
`@hasna-internal/kai-workflow-worker-thread` is the current engine and `@hasna-internal/kai-tool-workflow` is the model-facing consumer. A future process or sandbox engine can replace the implementation without changing the tool.
|
|
8
|
+
|
|
9
|
+
The package root is the Host face. The browser-safe `@hasna-internal/kai-workflow/types` subpath contains run identities, metadata, results, and observe-only lifecycle payloads without importing `Agent`, Cordis services, or Host context declarations; Host-only `WorkflowStartRequest` and `WorkflowRun` live behind the package root.
|
|
10
|
+
|
|
11
|
+
## Service and run contract
|
|
12
|
+
|
|
13
|
+
`WorkflowEngine.start(request): WorkflowRun` validates enough synchronously to reject a malformed meta block, unparseable script, unavailable provider route, or unsupported per-run limit before a run exists. Once returned, `WorkflowRun.result` never rejects: execution failures resolve with `stopReason: 'error'`, and cancellation resolves with `cancelled` within the engine's bounded grace.
|
|
14
|
+
|
|
15
|
+
A run is holder-owned. Engine-plugin unload prevents new starts but does not revoke accepted runs. The holder must call `dispose()` on every path; disposal cancels remaining work and reaches or abandons quiescence within the documented bound.
|
|
16
|
+
|
|
17
|
+
`WorkflowStartRequest` contains `{ meta, script, args?, subagentProvider?, maxTotalAgents?, parent, signal? }`. `parent` attributes every child agent to the invoking agent. `subagentProvider` optionally routes every child in that run without exposing provider choice to the script; omission uses the engine's configured provider. `maxTotalAgents` optionally lowers the engine's deployment ceiling for one run and is likewise invisible to the script. An implementation rejects invalid routes and limits synchronously. `meta` and `args` are plain data, not script fragments.
|
|
18
|
+
|
|
19
|
+
`WorkflowRun` exposes `{ id, meta, result, cancel(reason?), dispose() }`. `WorkflowResult` contains `{ value, stopReason, error?, agentsStarted }`; `value` is plain JSON data or `null`.
|
|
20
|
+
|
|
21
|
+
## Events
|
|
22
|
+
|
|
23
|
+
Workflow events are observe-only. They carry `WorkflowRunInfo` (`id` plus `meta`) rather than the live run, so listeners cannot acquire cancellation or disposal authority.
|
|
24
|
+
|
|
25
|
+
- `workflow/start` / `workflow/end` pair the run.
|
|
26
|
+
- `workflow/phase` and `workflow/log` expose script narration.
|
|
27
|
+
- `workflow/agent-start` / `workflow/agent-end` pair each child call by `seq`; a child whose async provider start rejects emits neither.
|
|
28
|
+
|
|
29
|
+
Same-process event payloads are borrowed immutable values. Every listener is independently contained: a synchronous throw or rejected returned promise is logged without starving peers or changing execution.
|
|
30
|
+
|
|
31
|
+
## Failure discipline
|
|
32
|
+
|
|
33
|
+
`WorkflowError` carries a code and a `fatal` flag. Fatal errors always escape `parallel()` and `pipeline()` instead of becoming an ordinary per-item `null`:
|
|
34
|
+
|
|
35
|
+
- `SCRIPT_PARSE` / `META_INVALID` — the workflow cannot start.
|
|
36
|
+
- `INVALID_ARGUMENT` / `UNSUPPORTED_OPTION` / `UNSUPPORTED_SCHEMA` — a hook call violates the engine contract.
|
|
37
|
+
- `AGENT_CAP` / `ITEM_CAP` — configured safety limits were exceeded.
|
|
38
|
+
- `AGENT_START` — the provider's async start rejected.
|
|
39
|
+
- `AGENT_RESULT` — a published child's result rejected with an infrastructure fault.
|
|
40
|
+
- `RESULT_UNSERIALIZABLE` — a script/worker value is not plain JSON data.
|
|
41
|
+
- `CANCELLED` — cancellation owns the run and pending/future hooks reject.
|
|
42
|
+
|
|
43
|
+
A child that resolves normally with a non-completed stop reason is not an infrastructure exception: `agent()` returns `null`, allowing the script to handle an ordinary child failure.
|
|
44
|
+
|
|
45
|
+
## Model Experience
|
|
46
|
+
|
|
47
|
+
Indirectly, through `dsh-tool-workflow` and a workflow engine, which create child-agent requests and return a retained parent tool result.
|
|
48
|
+
|
|
49
|
+
#### KV Cache effect
|
|
50
|
+
|
|
51
|
+
No direct invalidation; the named consumer owns any request-prefix changes.
|
|
52
|
+
|
|
53
|
+
## Known Limitations and Deferred Work
|
|
54
|
+
|
|
55
|
+
- **Foreground collection only** — the caller owns one live run and awaits it; background start/poll, spill handles, and detached collection are deferred.
|
|
56
|
+
- **No journaling or resume** — scripts, child progress, and intermediate values are not checkpointed, so a process restart cannot continue a run.
|
|
57
|
+
- **No saved or nested workflows** — the seam starts caller-supplied scripts only, and a workflow script receives no `workflow()` hook for recursive orchestration.
|
|
58
|
+
- **No token-budget vocabulary** — engines cap concurrency, items, and children, but neither the request nor result accounts for model tokens across children.
|
|
59
|
+
- **Runs are holder-owned, not service-tracked** — unloading the engine does not discover independent live handles; every consumer must dispose the run it started.
|
|
60
|
+
|
|
61
|
+
See the [dynamic-workflows Agent Note](../../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md) for the deferred workflow API.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @hasna-internal/kai-workflow
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
工作流 seam(扩展点,`ctx.workflowEngine`)执行由模型编写、可扇出 subagent 的编排脚本。该 seam 定义脚本、运行、结果、错误和事件契约;引擎负责决定如何隔离并执行脚本。
|
|
6
|
+
|
|
7
|
+
`@hasna-internal/kai-workflow-worker-thread` 是当前引擎,`@hasna-internal/kai-tool-workflow` 是面向模型的消费方。未来的进程或沙箱引擎可以替换实现,而无需更改工具。
|
|
8
|
+
|
|
9
|
+
包根是 Host face。浏览器安全的 `@hasna-internal/kai-workflow/types` 子路径包含运行身份、元数据、结果和仅供观察的生命周期 payload,不导入 `Agent`、Cordis service 或 Host Context 声明;Host 专用的 `WorkflowStartRequest` 与 `WorkflowRun` 只从包根提供。
|
|
10
|
+
|
|
11
|
+
## 服务与运行契约
|
|
12
|
+
|
|
13
|
+
`WorkflowEngine.start(request): WorkflowRun` 会同步完成足够多的校验,在运行创建前拒绝格式错误的 meta 块、无法解析的脚本、不可用的提供方路由或不受支持的单次运行限制。返回后,`WorkflowRun.result` 绝不拒绝:执行失败以 `stopReason: 'error'` 兑现,取消则在引擎有限的宽限时间内以 `cancelled` 兑现。
|
|
14
|
+
|
|
15
|
+
运行由持有方负责。引擎插件卸载会阻止新的启动,但不会撤销已接受的运行。持有方必须在每条路径上调用 `dispose()`;dispose(资源释放)会取消剩余工作,并在文档规定的期限内达到或放弃完全停稳。
|
|
16
|
+
|
|
17
|
+
`WorkflowStartRequest` 包含 `{ meta, script, args?, subagentProvider?, maxTotalAgents?, parent, signal? }`。`parent` 把每个子 agent(智能体)归属于调用 agent。`subagentProvider` 可以为该次运行的所有子 agent 指定路由,同时不向脚本公开提供方选择;省略时使用引擎配置的提供方。`maxTotalAgents` 可以为一次运行降低引擎的部署上限,同样对脚本不可见。实现会同步拒绝无效路由和限制。`meta` 与 `args` 是普通数据,不是脚本片段。
|
|
18
|
+
|
|
19
|
+
`WorkflowRun` 公开 `{ id, meta, result, cancel(reason?), dispose() }`。`WorkflowResult` 包含 `{ value, stopReason, error?, agentsStarted }`;`value` 是普通 JSON 数据或 `null`。
|
|
20
|
+
|
|
21
|
+
## 事件
|
|
22
|
+
|
|
23
|
+
工作流事件只供观察。它们携带 `WorkflowRunInfo`(`id` 加 `meta`),而不是活动运行,因此监听器无法取得取消或 dispose 权限。
|
|
24
|
+
|
|
25
|
+
- `workflow/start` / `workflow/end` 为运行配对;
|
|
26
|
+
- `workflow/phase` 和 `workflow/log` 公开脚本叙述;
|
|
27
|
+
- `workflow/agent-start` / `workflow/agent-end` 按 `seq` 为每次子 agent 调用配对;提供方的异步启动调用被拒绝时,该子 agent 不会发出其中任何一个事件。
|
|
28
|
+
|
|
29
|
+
同进程事件 payload 是以不可变方式借用的值。每个监听器都独立隔离:同步抛出异常或返回的 promise 被拒绝时,只会记录日志,不会阻塞同级监听器或改变执行。
|
|
30
|
+
|
|
31
|
+
## 失败纪律
|
|
32
|
+
|
|
33
|
+
`WorkflowError` 携带一个代码和 `fatal` 标志。致命错误总会逸出 `parallel()` 和 `pipeline()`,而不会变成普通的逐项 `null`:
|
|
34
|
+
|
|
35
|
+
- `SCRIPT_PARSE` / `META_INVALID`:工作流无法启动;
|
|
36
|
+
- `INVALID_ARGUMENT` / `UNSUPPORTED_OPTION` / `UNSUPPORTED_SCHEMA`:钩子调用违反引擎契约;
|
|
37
|
+
- `AGENT_CAP` / `ITEM_CAP`:超过已配置的安全上限;
|
|
38
|
+
- `AGENT_START`:提供方的异步启动调用被拒绝;
|
|
39
|
+
- `AGENT_RESULT`:已发布子 agent 的结果因基础设施故障而被拒绝;
|
|
40
|
+
- `RESULT_UNSERIALIZABLE`:脚本/worker 值不是普通 JSON 数据;
|
|
41
|
+
- `CANCELLED`:取消会接管该运行,待处理和未来的钩子都会拒绝。
|
|
42
|
+
|
|
43
|
+
子 agent 若以非完成的结束原因正常兑现,并不属于基础设施异常:`agent()` 返回 `null`,使脚本可以处理普通的子 agent 失败。
|
|
44
|
+
|
|
45
|
+
## 模型体验
|
|
46
|
+
|
|
47
|
+
通过 `dsh-tool-workflow` 和工作流引擎间接产生影响;两者创建子 agent 请求,并返回保留在父级的工具结果。
|
|
48
|
+
|
|
49
|
+
#### KV Cache 影响
|
|
50
|
+
|
|
51
|
+
不会直接导致 KV Cache 失效;请求前缀的任何变化均由上述消费方负责。
|
|
52
|
+
|
|
53
|
+
## 已知限制与暂缓事项
|
|
54
|
+
|
|
55
|
+
- **仅支持前台收集**:调用方负责一个活动运行并等待它;后台启动/轮询、spill 句柄和分离收集均暂缓处理。
|
|
56
|
+
- **没有日志化或恢复**:脚本、子 agent 进度和中间值均不设检查点,因此进程重启后无法继续运行。
|
|
57
|
+
- **没有已保存或嵌套工作流**:该 seam 只启动调用方提供的脚本,工作流脚本不会收到用于递归编排的 `workflow()` 钩子。
|
|
58
|
+
- **没有 token 预算词汇**:引擎会限制并发、条目和子 agent,但请求与结果都不会统计跨子 agent 的模型 token。
|
|
59
|
+
- **运行由持有方负责,不由服务跟踪**:卸载引擎不会发现独立的活动句柄;每个消费方都必须 dispose 自己启动的运行。
|
|
60
|
+
|
|
61
|
+
暂缓实现的工作流接口见[动态工作流 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.zh.md)。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import { HarnessError } from "@hasna-internal/kai-llm";
|
|
3
|
+
//#region lib/types/types.js
|
|
4
|
+
/**
|
|
5
|
+
* Workflow seam vocabulary: the request/run/result types a workflow engine
|
|
6
|
+
* consumes and produces, plus the fields in the `workflow/*` event payloads.
|
|
7
|
+
* Types only (plus the id-brand factory), per the package convention.
|
|
8
|
+
*
|
|
9
|
+
* @module @hasna-internal/kai-workflow/types
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Brand a string as a {@link WorkflowRunId}.
|
|
13
|
+
* @param id - the raw id string (the engine mints UUIDs; tests may pass fixtures).
|
|
14
|
+
* @returns the same string, branded.
|
|
15
|
+
*/
|
|
16
|
+
function WorkflowRunId(id) {
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region lib/types/index.js
|
|
21
|
+
/**
|
|
22
|
+
* Service Definition for the workflow capability seam. Service Providers execute orchestration scripts;
|
|
23
|
+
* observe-only lifecycle events never expose run control.
|
|
24
|
+
* @module @hasna-internal/kai-workflow
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Typed error for workflow-seam failures. Extends {@link HarnessError}, so the
|
|
28
|
+
* `code` is machine-routable taxonomy. `fatal` drives the combinator
|
|
29
|
+
* discipline: `parallel()`/`pipeline()` re-throw a fatal error (a typo'd
|
|
30
|
+
* option or a tripped cap must kill the script loudly), and reserve the
|
|
31
|
+
* per-item `null` for child-run failures and ordinary in-stage script errors.
|
|
32
|
+
* Every {@link WorkflowErrorCode} is fatal; the flag exists so the
|
|
33
|
+
* distinction is explicit at every catch site rather than implied.
|
|
34
|
+
*/
|
|
35
|
+
var WorkflowError = class extends HarnessError {
|
|
36
|
+
/** Whether combinators must propagate this error instead of nulling the item. */
|
|
37
|
+
fatal;
|
|
38
|
+
constructor(message, code, options) {
|
|
39
|
+
super(message, code, options);
|
|
40
|
+
this.name = "WorkflowError";
|
|
41
|
+
this.fatal = options?.fatal ?? true;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Whether combinators must re-throw `error` instead of mapping the item to `null`.
|
|
46
|
+
* @param error - any thrown value; fatality is host `instanceof` (unforgeable from a script realm).
|
|
47
|
+
* @returns true iff `error` is a {@link WorkflowError} whose `fatal` flag is set.
|
|
48
|
+
*/
|
|
49
|
+
function isFatalWorkflowError(error) {
|
|
50
|
+
return error instanceof WorkflowError && error.fatal;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Workflow Service Definition contract. Invalid requests throw before publication; a live
|
|
54
|
+
* run is holder-owned, its result never rejects, cancellation and disposal are
|
|
55
|
+
* bounded, and disposal waits for child cleanup within that bound. Lifecycle
|
|
56
|
+
* listener failures are contained, and `workflow/end` fires exactly once as the
|
|
57
|
+
* result settles.
|
|
58
|
+
*/
|
|
59
|
+
var WorkflowEngine = class extends Service {
|
|
60
|
+
constructor(ctx) {
|
|
61
|
+
super(ctx, "workflowEngine");
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Emit a lifecycle event while containing and logging each listener failure.
|
|
65
|
+
* @param name - the `workflow/*` event to dispatch.
|
|
66
|
+
* @param args - the event's payload, matching its declared signature.
|
|
67
|
+
*/
|
|
68
|
+
emitWorkflowEvent(name, ...args) {
|
|
69
|
+
for (const callback of this.ctx.events.dispatch("emit", [name, ...args])) try {
|
|
70
|
+
const returned = callback(...args);
|
|
71
|
+
Promise.resolve(returned).catch((error) => {
|
|
72
|
+
this.ctx.logger.warn(`workflow: ${name} listener rejected: ${renderListenerError(error)}`);
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.ctx.logger.warn(`workflow: ${name} listener threw: ${renderListenerError(error)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Render any thrown value without violating listener containment.
|
|
81
|
+
* @param error - any thrown value.
|
|
82
|
+
* @returns `String(error)`, or a fixed label when even coercion throws.
|
|
83
|
+
*/
|
|
84
|
+
function renderListenerError(error) {
|
|
85
|
+
try {
|
|
86
|
+
return String(error);
|
|
87
|
+
} catch {
|
|
88
|
+
return "[unrenderable thrown value]";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { WorkflowEngine, WorkflowEngine as default, WorkflowError, WorkflowRunId, isFatalWorkflowError };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/** Package-owned workflow lifecycle invariants. @module @hasna-internal/kai-workflow/invariant */
|
|
3
|
+
const PACKAGE_NAME = "@hasna-internal/kai-workflow";
|
|
4
|
+
/** Cordis companion plugin name. */
|
|
5
|
+
const name = "workflow-invariant";
|
|
6
|
+
/** Service required before the companion can reserve package ownership. */
|
|
7
|
+
const inject = ["invariants"];
|
|
8
|
+
/** Require every event for a run to retain its validated identity snapshot. */
|
|
9
|
+
function traceFor(traces, info, fail) {
|
|
10
|
+
const trace = traces.get(info.id);
|
|
11
|
+
if (trace === void 0) fail(`workflow event has no matching workflow/start for run ${JSON.stringify(info.id)}`);
|
|
12
|
+
if (trace.meta !== JSON.stringify(info.meta)) fail(`workflow event meta diverges from workflow/start for run ${JSON.stringify(info.id)}`);
|
|
13
|
+
return trace;
|
|
14
|
+
}
|
|
15
|
+
/** Assert the immutable identity fields shared by an agent pair. */
|
|
16
|
+
function validateAgentEnd(start, end, fail) {
|
|
17
|
+
if (start.label !== end.label || start.phase !== end.phase || start.childId !== end.childId) fail(`workflow/agent-end identity diverges from workflow/agent-start for seq ${end.seq}`);
|
|
18
|
+
const outcome = end.outcome;
|
|
19
|
+
if (outcome !== "completed" && outcome !== "failed" && outcome !== "cancelled") fail(`workflow/agent-end carries unknown outcome ${JSON.stringify(outcome)}`);
|
|
20
|
+
}
|
|
21
|
+
/** Validate a terminal result against the accumulated run trace. */
|
|
22
|
+
function validateWorkflowEnd(trace, result, fail) {
|
|
23
|
+
if (trace.agents.size > 0) fail(`workflow/end has ${trace.agents.size} agent call(s) without workflow/agent-end`);
|
|
24
|
+
if (!Number.isSafeInteger(result.agentsStarted) || result.agentsStarted < trace.starts) fail("workflow/end agentsStarted must be a safe integer covering every observed agent start");
|
|
25
|
+
if (result.stopReason === "completed" ? result.error !== void 0 : typeof result.error !== "string") fail("workflow/end error must be absent exactly for completed runs");
|
|
26
|
+
}
|
|
27
|
+
/** Install workflow start/end and child-call pairing checks. */
|
|
28
|
+
const install = (ctx, fail) => {
|
|
29
|
+
const traces = /* @__PURE__ */ new Map();
|
|
30
|
+
const stagedStarts = /* @__PURE__ */ new WeakSet();
|
|
31
|
+
const stagedAgentStarts = /* @__PURE__ */ new WeakSet();
|
|
32
|
+
const stagedAgentEnds = /* @__PURE__ */ new WeakSet();
|
|
33
|
+
const stagedEnds = /* @__PURE__ */ new WeakSet();
|
|
34
|
+
ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
35
|
+
if (eventName === "workflow/start") {
|
|
36
|
+
const info = args[0];
|
|
37
|
+
if (String(info.id).length === 0 || info.meta.name.length === 0 || info.meta.description.length === 0) fail("workflow/start id, meta.name, and meta.description must be non-empty");
|
|
38
|
+
if (traces.has(info.id)) fail(`workflow/start repeated run id ${JSON.stringify(info.id)}`);
|
|
39
|
+
stagedStarts.add(info);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!eventName.startsWith("workflow/")) return;
|
|
43
|
+
const info = args[0];
|
|
44
|
+
const trace = traceFor(traces, info, fail);
|
|
45
|
+
if (eventName === "workflow/agent-start") {
|
|
46
|
+
const agent = args[1];
|
|
47
|
+
if (!Number.isSafeInteger(agent.seq) || agent.seq < 1 || String(agent.childId).length === 0) fail("workflow/agent-start seq must be positive and childId must be non-empty");
|
|
48
|
+
if (trace.agents.has(agent.seq)) fail(`workflow/agent-start repeated seq ${agent.seq}`);
|
|
49
|
+
stagedAgentStarts.add(agent);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (eventName === "workflow/agent-end") {
|
|
53
|
+
const agent = args[1];
|
|
54
|
+
const start = trace.agents.get(agent.seq);
|
|
55
|
+
if (start === void 0) return fail(`workflow/agent-end has no matching start for seq ${agent.seq}`);
|
|
56
|
+
validateAgentEnd(start, agent, fail);
|
|
57
|
+
stagedAgentEnds.add(agent);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (eventName === "workflow/end") {
|
|
61
|
+
const result = args[1];
|
|
62
|
+
validateWorkflowEnd(trace, result, fail);
|
|
63
|
+
stagedEnds.add(result);
|
|
64
|
+
}
|
|
65
|
+
}, { global: true });
|
|
66
|
+
ctx.on("workflow/start", (info) => {
|
|
67
|
+
/* v8 ignore next -- internal/dispatch stages the same run-info object */
|
|
68
|
+
if (!stagedStarts.delete(info)) return;
|
|
69
|
+
traces.set(info.id, {
|
|
70
|
+
meta: JSON.stringify(info.meta),
|
|
71
|
+
agents: /* @__PURE__ */ new Map(),
|
|
72
|
+
starts: 0
|
|
73
|
+
});
|
|
74
|
+
}, { global: true });
|
|
75
|
+
ctx.on("workflow/agent-start", (info, agent) => {
|
|
76
|
+
/* v8 ignore next -- internal/dispatch stages the same agent object */
|
|
77
|
+
if (!stagedAgentStarts.delete(agent)) return;
|
|
78
|
+
const trace = traceFor(traces, info, fail);
|
|
79
|
+
trace.agents.set(agent.seq, agent);
|
|
80
|
+
trace.starts += 1;
|
|
81
|
+
}, { global: true });
|
|
82
|
+
ctx.on("workflow/agent-end", (info, agent) => {
|
|
83
|
+
/* v8 ignore next -- internal/dispatch stages the same agent object */
|
|
84
|
+
if (!stagedAgentEnds.delete(agent)) return;
|
|
85
|
+
traceFor(traces, info, fail).agents.delete(agent.seq);
|
|
86
|
+
}, { global: true });
|
|
87
|
+
ctx.on("workflow/end", (info, result) => {
|
|
88
|
+
/* v8 ignore next -- internal/dispatch stages the same result object */
|
|
89
|
+
if (!stagedEnds.delete(result)) return;
|
|
90
|
+
traces.delete(info.id);
|
|
91
|
+
}, { global: true });
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Register the workflow invariant companion.
|
|
95
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
96
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
97
|
+
*/
|
|
98
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
99
|
+
//#endregion
|
|
100
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Definition for the workflow capability seam. Service Providers execute orchestration scripts;
|
|
3
|
+
* observe-only lifecycle events never expose run control.
|
|
4
|
+
* @module @hasna-internal/kai-workflow
|
|
5
|
+
*/
|
|
6
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
7
|
+
import { HarnessError } from '@hasna-internal/kai-llm';
|
|
8
|
+
import type { WorkflowAgentEndInfo, WorkflowAgentInfo, WorkflowResultInfo, WorkflowRunInfo } from './types.ts';
|
|
9
|
+
import type { WorkflowRun, WorkflowStartRequest } from './runtime-types.ts';
|
|
10
|
+
export { WorkflowRunId } from './types.ts';
|
|
11
|
+
export type { WorkflowAgentEndInfo, WorkflowAgentInfo, WorkflowAgentOutcome, WorkflowMeta, WorkflowPhase, WorkflowResult, WorkflowResultInfo, WorkflowRunInfo, WorkflowStopReason, } from './types.ts';
|
|
12
|
+
export type { WorkflowRun, WorkflowStartRequest } from './runtime-types.ts';
|
|
13
|
+
declare module '@deepseek-ai/cordis' {
|
|
14
|
+
interface Context {
|
|
15
|
+
workflowEngine: WorkflowEngine;
|
|
16
|
+
}
|
|
17
|
+
interface Events {
|
|
18
|
+
/**
|
|
19
|
+
* A workflow run started — the script's meta block validated, the body
|
|
20
|
+
* about to execute. Paired with {@link Events['workflow/end']}.
|
|
21
|
+
* @param info - the run's identity snapshot (id + meta).
|
|
22
|
+
* @mode emit
|
|
23
|
+
*/
|
|
24
|
+
'workflow/start'(info: WorkflowRunInfo): void;
|
|
25
|
+
/**
|
|
26
|
+
* The script entered a phase (a `phase(title)` call) — progress grouping
|
|
27
|
+
* for observers; no execution semantics.
|
|
28
|
+
* @param info - the run's identity snapshot.
|
|
29
|
+
* @param title - the phase title, verbatim.
|
|
30
|
+
* @mode emit
|
|
31
|
+
*/
|
|
32
|
+
'workflow/phase'(info: WorkflowRunInfo, title: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* The script emitted a narration line (a `log(message)` call).
|
|
35
|
+
* @param info - the run's identity snapshot.
|
|
36
|
+
* @param message - the logged message, verbatim.
|
|
37
|
+
* @mode emit
|
|
38
|
+
*/
|
|
39
|
+
'workflow/log'(info: WorkflowRunInfo, message: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* One `agent()` call established a published child run. Paired with
|
|
42
|
+
* {@link Events['workflow/agent-end']} by `agent.seq`. A call that never
|
|
43
|
+
* receives a published run from the provider emits neither
|
|
44
|
+
* event in this pair.
|
|
45
|
+
* @param info - the run's identity snapshot.
|
|
46
|
+
* @param agent - the call's sequence number, label, phase, and child id.
|
|
47
|
+
* @mode emit
|
|
48
|
+
*/
|
|
49
|
+
'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void;
|
|
50
|
+
/**
|
|
51
|
+
* One `agent()` call settled (clean result, child failure, or run
|
|
52
|
+
* cancellation). Paired with {@link Events['workflow/agent-start']} by
|
|
53
|
+
* `agent.seq`, exactly once per started call on every stop path — on an
|
|
54
|
+
* engine termination path (a worker killed past its grace) the end is
|
|
55
|
+
* engine-synthesized with outcome `'cancelled'`.
|
|
56
|
+
* @param info - the run's identity snapshot.
|
|
57
|
+
* @param agent - the call identity plus its outcome.
|
|
58
|
+
* @mode emit
|
|
59
|
+
*/
|
|
60
|
+
'workflow/agent-end'(info: WorkflowRunInfo, agent: WorkflowAgentEndInfo): void;
|
|
61
|
+
/**
|
|
62
|
+
* A workflow run settled (any stop reason). Fired when
|
|
63
|
+
* {@link WorkflowRun.result} resolves. Paired with
|
|
64
|
+
* {@link Events['workflow/start']}.
|
|
65
|
+
* @param info - the run's identity snapshot.
|
|
66
|
+
* @param result - the outcome data (stop reason, error, agent count) —
|
|
67
|
+
* deliberately WITHOUT the result value (see {@link WorkflowResultInfo}).
|
|
68
|
+
* @mode emit
|
|
69
|
+
*/
|
|
70
|
+
'workflow/end'(info: WorkflowRunInfo, result: WorkflowResultInfo): void;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** The full set of `workflow/*` event names {@link WorkflowEngine.emitWorkflowEvent} dispatches. */
|
|
74
|
+
export type WorkflowEventName = 'workflow/start' | 'workflow/phase' | 'workflow/log' | 'workflow/agent-start' | 'workflow/agent-end' | 'workflow/end';
|
|
75
|
+
/**
|
|
76
|
+
* Machine-routable fatal workflow failures: parse/meta/argument/schema errors,
|
|
77
|
+
* resource caps, subagent infrastructure failures, unserializable boundary
|
|
78
|
+
* values, and cancellation. An ordinary child failure resolves its item to
|
|
79
|
+
* `null` and is not one of these fatal codes.
|
|
80
|
+
*/
|
|
81
|
+
export type WorkflowErrorCode = 'SCRIPT_PARSE' | 'META_INVALID' | 'INVALID_ARGUMENT' | 'UNSUPPORTED_OPTION' | 'UNSUPPORTED_SCHEMA' | 'AGENT_CAP' | 'ITEM_CAP' | 'AGENT_START' | 'AGENT_RESULT' | 'RESULT_UNSERIALIZABLE' | 'CANCELLED';
|
|
82
|
+
/**
|
|
83
|
+
* Typed error for workflow-seam failures. Extends {@link HarnessError}, so the
|
|
84
|
+
* `code` is machine-routable taxonomy. `fatal` drives the combinator
|
|
85
|
+
* discipline: `parallel()`/`pipeline()` re-throw a fatal error (a typo'd
|
|
86
|
+
* option or a tripped cap must kill the script loudly), and reserve the
|
|
87
|
+
* per-item `null` for child-run failures and ordinary in-stage script errors.
|
|
88
|
+
* Every {@link WorkflowErrorCode} is fatal; the flag exists so the
|
|
89
|
+
* distinction is explicit at every catch site rather than implied.
|
|
90
|
+
*/
|
|
91
|
+
export declare class WorkflowError extends HarnessError {
|
|
92
|
+
/** Whether combinators must propagate this error instead of nulling the item. */
|
|
93
|
+
readonly fatal: boolean;
|
|
94
|
+
constructor(message: string, code: WorkflowErrorCode, options?: ErrorOptions & {
|
|
95
|
+
fatal?: boolean;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Whether combinators must re-throw `error` instead of mapping the item to `null`.
|
|
100
|
+
* @param error - any thrown value; fatality is host `instanceof` (unforgeable from a script realm).
|
|
101
|
+
* @returns true iff `error` is a {@link WorkflowError} whose `fatal` flag is set.
|
|
102
|
+
*/
|
|
103
|
+
export declare function isFatalWorkflowError(error: unknown): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Workflow Service Definition contract. Invalid requests throw before publication; a live
|
|
106
|
+
* run is holder-owned, its result never rejects, cancellation and disposal are
|
|
107
|
+
* bounded, and disposal waits for child cleanup within that bound. Lifecycle
|
|
108
|
+
* listener failures are contained, and `workflow/end` fires exactly once as the
|
|
109
|
+
* result settles.
|
|
110
|
+
*/
|
|
111
|
+
export declare abstract class WorkflowEngine extends Service {
|
|
112
|
+
constructor(ctx: Context);
|
|
113
|
+
/**
|
|
114
|
+
* Parse and execute a workflow script.
|
|
115
|
+
* @param request - the script, its `args`, the parent agent, and an
|
|
116
|
+
* optional cancel signal.
|
|
117
|
+
* @returns the live run; its `result` resolves when the script settles.
|
|
118
|
+
*/
|
|
119
|
+
abstract start(request: WorkflowStartRequest): WorkflowRun;
|
|
120
|
+
/**
|
|
121
|
+
* Emit a lifecycle event while containing and logging each listener failure.
|
|
122
|
+
* @param name - the `workflow/*` event to dispatch.
|
|
123
|
+
* @param args - the event's payload, matching its declared signature.
|
|
124
|
+
*/
|
|
125
|
+
protected emitWorkflowEvent(name: WorkflowEventName, ...args: unknown[]): void;
|
|
126
|
+
}
|
|
127
|
+
export default WorkflowEngine;
|
|
128
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Definition for the workflow capability seam. Service Providers execute orchestration scripts;
|
|
3
|
+
* observe-only lifecycle events never expose run control.
|
|
4
|
+
* @module @hasna-internal/kai-workflow
|
|
5
|
+
*/
|
|
6
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
7
|
+
import { HarnessError } from '@hasna-internal/kai-llm';
|
|
8
|
+
export { WorkflowRunId } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Typed error for workflow-seam failures. Extends {@link HarnessError}, so the
|
|
11
|
+
* `code` is machine-routable taxonomy. `fatal` drives the combinator
|
|
12
|
+
* discipline: `parallel()`/`pipeline()` re-throw a fatal error (a typo'd
|
|
13
|
+
* option or a tripped cap must kill the script loudly), and reserve the
|
|
14
|
+
* per-item `null` for child-run failures and ordinary in-stage script errors.
|
|
15
|
+
* Every {@link WorkflowErrorCode} is fatal; the flag exists so the
|
|
16
|
+
* distinction is explicit at every catch site rather than implied.
|
|
17
|
+
*/
|
|
18
|
+
export class WorkflowError extends HarnessError {
|
|
19
|
+
/** Whether combinators must propagate this error instead of nulling the item. */
|
|
20
|
+
fatal;
|
|
21
|
+
constructor(message, code, options) {
|
|
22
|
+
super(message, code, options);
|
|
23
|
+
this.name = 'WorkflowError';
|
|
24
|
+
this.fatal = options?.fatal ?? true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether combinators must re-throw `error` instead of mapping the item to `null`.
|
|
29
|
+
* @param error - any thrown value; fatality is host `instanceof` (unforgeable from a script realm).
|
|
30
|
+
* @returns true iff `error` is a {@link WorkflowError} whose `fatal` flag is set.
|
|
31
|
+
*/
|
|
32
|
+
export function isFatalWorkflowError(error) {
|
|
33
|
+
return error instanceof WorkflowError && error.fatal;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Workflow Service Definition contract. Invalid requests throw before publication; a live
|
|
37
|
+
* run is holder-owned, its result never rejects, cancellation and disposal are
|
|
38
|
+
* bounded, and disposal waits for child cleanup within that bound. Lifecycle
|
|
39
|
+
* listener failures are contained, and `workflow/end` fires exactly once as the
|
|
40
|
+
* result settles.
|
|
41
|
+
*/
|
|
42
|
+
export class WorkflowEngine extends Service {
|
|
43
|
+
constructor(ctx) {
|
|
44
|
+
super(ctx, 'workflowEngine');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Emit a lifecycle event while containing and logging each listener failure.
|
|
48
|
+
* @param name - the `workflow/*` event to dispatch.
|
|
49
|
+
* @param args - the event's payload, matching its declared signature.
|
|
50
|
+
*/
|
|
51
|
+
emitWorkflowEvent(name, ...args) {
|
|
52
|
+
for (const callback of this.ctx.events.dispatch('emit', [name, ...args])) {
|
|
53
|
+
try {
|
|
54
|
+
const returned = callback(...args);
|
|
55
|
+
void Promise.resolve(returned).catch((error) => {
|
|
56
|
+
this.ctx.logger.warn(`workflow: ${name} listener rejected: ${renderListenerError(error)}`);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
this.ctx.logger.warn(`workflow: ${name} listener threw: ${renderListenerError(error)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Render any thrown value without violating listener containment.
|
|
67
|
+
* @param error - any thrown value.
|
|
68
|
+
* @returns `String(error)`, or a fixed label when even coercion throws.
|
|
69
|
+
*/
|
|
70
|
+
function renderListenerError(error) {
|
|
71
|
+
try {
|
|
72
|
+
return String(error);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// String coercion itself may throw.
|
|
76
|
+
return '[unrenderable thrown value]';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export default WorkflowEngine;
|
|
80
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Package-owned workflow lifecycle invariants. @module @hasna-internal/kai-workflow/invariant */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export declare const name = "workflow-invariant";
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
export declare const inject: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Register the workflow invariant companion.
|
|
9
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
10
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
11
|
+
*/
|
|
12
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
13
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Package-owned workflow lifecycle invariants. @module @hasna-internal/kai-workflow/invariant */
|
|
2
|
+
const PACKAGE_NAME = '@hasna-internal/kai-workflow';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export const name = 'workflow-invariant';
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
export const inject = ['invariants'];
|
|
7
|
+
/** Require every event for a run to retain its validated identity snapshot. */
|
|
8
|
+
function traceFor(traces, info, fail) {
|
|
9
|
+
const trace = traces.get(info.id);
|
|
10
|
+
if (trace === undefined)
|
|
11
|
+
fail(`workflow event has no matching workflow/start for run ${JSON.stringify(info.id)}`);
|
|
12
|
+
if (trace.meta !== JSON.stringify(info.meta)) {
|
|
13
|
+
fail(`workflow event meta diverges from workflow/start for run ${JSON.stringify(info.id)}`);
|
|
14
|
+
}
|
|
15
|
+
return trace;
|
|
16
|
+
}
|
|
17
|
+
/** Assert the immutable identity fields shared by an agent pair. */
|
|
18
|
+
function validateAgentEnd(start, end, fail) {
|
|
19
|
+
if (start.label !== end.label || start.phase !== end.phase || start.childId !== end.childId) {
|
|
20
|
+
fail(`workflow/agent-end identity diverges from workflow/agent-start for seq ${end.seq}`);
|
|
21
|
+
}
|
|
22
|
+
const outcome = end.outcome;
|
|
23
|
+
if (outcome !== 'completed' && outcome !== 'failed' && outcome !== 'cancelled') {
|
|
24
|
+
fail(`workflow/agent-end carries unknown outcome ${JSON.stringify(outcome)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Validate a terminal result against the accumulated run trace. */
|
|
28
|
+
function validateWorkflowEnd(trace, result, fail) {
|
|
29
|
+
if (trace.agents.size > 0)
|
|
30
|
+
fail(`workflow/end has ${trace.agents.size} agent call(s) without workflow/agent-end`);
|
|
31
|
+
if (!Number.isSafeInteger(result.agentsStarted) || result.agentsStarted < trace.starts) {
|
|
32
|
+
fail('workflow/end agentsStarted must be a safe integer covering every observed agent start');
|
|
33
|
+
}
|
|
34
|
+
if (result.stopReason === 'completed' ? result.error !== undefined : typeof result.error !== 'string') {
|
|
35
|
+
fail('workflow/end error must be absent exactly for completed runs');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Install workflow start/end and child-call pairing checks. */
|
|
39
|
+
const install = (ctx, fail) => {
|
|
40
|
+
const traces = new Map();
|
|
41
|
+
const stagedStarts = new WeakSet();
|
|
42
|
+
const stagedAgentStarts = new WeakSet();
|
|
43
|
+
const stagedAgentEnds = new WeakSet();
|
|
44
|
+
const stagedEnds = new WeakSet();
|
|
45
|
+
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
|
46
|
+
if (eventName === 'workflow/start') {
|
|
47
|
+
const info = args[0];
|
|
48
|
+
if (String(info.id).length === 0 || info.meta.name.length === 0 || info.meta.description.length === 0) {
|
|
49
|
+
fail('workflow/start id, meta.name, and meta.description must be non-empty');
|
|
50
|
+
}
|
|
51
|
+
if (traces.has(info.id))
|
|
52
|
+
fail(`workflow/start repeated run id ${JSON.stringify(info.id)}`);
|
|
53
|
+
stagedStarts.add(info);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!eventName.startsWith('workflow/'))
|
|
57
|
+
return;
|
|
58
|
+
const info = args[0];
|
|
59
|
+
const trace = traceFor(traces, info, fail);
|
|
60
|
+
if (eventName === 'workflow/agent-start') {
|
|
61
|
+
const agent = args[1];
|
|
62
|
+
if (!Number.isSafeInteger(agent.seq) || agent.seq < 1 || String(agent.childId).length === 0) {
|
|
63
|
+
fail('workflow/agent-start seq must be positive and childId must be non-empty');
|
|
64
|
+
}
|
|
65
|
+
if (trace.agents.has(agent.seq))
|
|
66
|
+
fail(`workflow/agent-start repeated seq ${agent.seq}`);
|
|
67
|
+
stagedAgentStarts.add(agent);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (eventName === 'workflow/agent-end') {
|
|
71
|
+
const agent = args[1];
|
|
72
|
+
const start = trace.agents.get(agent.seq);
|
|
73
|
+
if (start === undefined)
|
|
74
|
+
return fail(`workflow/agent-end has no matching start for seq ${agent.seq}`);
|
|
75
|
+
validateAgentEnd(start, agent, fail);
|
|
76
|
+
stagedAgentEnds.add(agent);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (eventName === 'workflow/end') {
|
|
80
|
+
const result = args[1];
|
|
81
|
+
validateWorkflowEnd(trace, result, fail);
|
|
82
|
+
stagedEnds.add(result);
|
|
83
|
+
}
|
|
84
|
+
}, { global: true });
|
|
85
|
+
ctx.on('workflow/start', (info) => {
|
|
86
|
+
/* v8 ignore next -- internal/dispatch stages the same run-info object */
|
|
87
|
+
if (!stagedStarts.delete(info))
|
|
88
|
+
return;
|
|
89
|
+
traces.set(info.id, { meta: JSON.stringify(info.meta), agents: new Map(), starts: 0 });
|
|
90
|
+
}, { global: true });
|
|
91
|
+
ctx.on('workflow/agent-start', (info, agent) => {
|
|
92
|
+
/* v8 ignore next -- internal/dispatch stages the same agent object */
|
|
93
|
+
if (!stagedAgentStarts.delete(agent))
|
|
94
|
+
return;
|
|
95
|
+
const trace = traceFor(traces, info, fail);
|
|
96
|
+
trace.agents.set(agent.seq, agent);
|
|
97
|
+
trace.starts += 1;
|
|
98
|
+
}, { global: true });
|
|
99
|
+
ctx.on('workflow/agent-end', (info, agent) => {
|
|
100
|
+
/* v8 ignore next -- internal/dispatch stages the same agent object */
|
|
101
|
+
if (!stagedAgentEnds.delete(agent))
|
|
102
|
+
return;
|
|
103
|
+
traceFor(traces, info, fail).agents.delete(agent.seq);
|
|
104
|
+
}, { global: true });
|
|
105
|
+
ctx.on('workflow/end', (info, result) => {
|
|
106
|
+
/* v8 ignore next -- internal/dispatch stages the same result object */
|
|
107
|
+
if (!stagedEnds.delete(result))
|
|
108
|
+
return;
|
|
109
|
+
traces.delete(info.id);
|
|
110
|
+
}, { global: true });
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Register the workflow invariant companion.
|
|
114
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
115
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
116
|
+
*/
|
|
117
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
118
|
+
//# sourceMappingURL=invariant.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-only workflow request and live-run handles. The browser-safe durable
|
|
3
|
+
* vocabulary remains in `./types` so Client programs never import Agent or
|
|
4
|
+
* host Cordis context declarations.
|
|
5
|
+
*
|
|
6
|
+
* @module @hasna-internal/kai-workflow
|
|
7
|
+
*/
|
|
8
|
+
import type { Agent } from '@hasna-internal/kai-agent';
|
|
9
|
+
import type { WorkflowMeta, WorkflowResult, WorkflowRunId } from './types.ts';
|
|
10
|
+
/**
|
|
11
|
+
* What a caller asks for when starting a workflow run. `meta` and `args` are
|
|
12
|
+
* plain JSON data by the seam contract. `parent` is required because every
|
|
13
|
+
* `agent()` spawned by the script is attributed to that live Agent.
|
|
14
|
+
*/
|
|
15
|
+
export interface WorkflowStartRequest {
|
|
16
|
+
/** The plain-JS script body (top-level await allowed; ends with `return <json-value>`). */
|
|
17
|
+
script: string;
|
|
18
|
+
/** The workflow's identity block, as plain JSON data (shape-validated by the engine). */
|
|
19
|
+
meta: WorkflowMeta;
|
|
20
|
+
/** Optional input exposed verbatim to the script as the `args` global. */
|
|
21
|
+
args?: unknown;
|
|
22
|
+
/** Optional engine-wide child-provider override for this run. */
|
|
23
|
+
subagentProvider?: string;
|
|
24
|
+
/** Optional per-run total-child ceiling. */
|
|
25
|
+
maxTotalAgents?: number;
|
|
26
|
+
/** The agent on whose behalf the run executes (parent of every child). */
|
|
27
|
+
parent: Agent;
|
|
28
|
+
/** Cancels the run when aborted. */
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Holder-owned live workflow. `result` never rejects; consumers may cancel
|
|
33
|
+
* and must call idempotent `dispose()` to await script and child quiescence.
|
|
34
|
+
*/
|
|
35
|
+
export interface WorkflowRun {
|
|
36
|
+
readonly id: WorkflowRunId;
|
|
37
|
+
/** The validated meta block available before the script body runs. */
|
|
38
|
+
readonly meta: WorkflowMeta;
|
|
39
|
+
readonly result: Promise<WorkflowResult>;
|
|
40
|
+
/** Cancel the run and its children. */
|
|
41
|
+
cancel(reason?: string): void;
|
|
42
|
+
/** Cancel if needed and await bounded settlement and cleanup. */
|
|
43
|
+
dispose(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=runtime-types.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-only workflow request and live-run handles. The browser-safe durable
|
|
3
|
+
* vocabulary remains in `./types` so Client programs never import Agent or
|
|
4
|
+
* host Cordis context declarations.
|
|
5
|
+
*
|
|
6
|
+
* @module @hasna-internal/kai-workflow
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=runtime-types.js.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow seam vocabulary: the request/run/result types a workflow engine
|
|
3
|
+
* consumes and produces, plus the fields in the `workflow/*` event payloads.
|
|
4
|
+
* Types only (plus the id-brand factory), per the package convention.
|
|
5
|
+
*
|
|
6
|
+
* @module @hasna-internal/kai-workflow/types
|
|
7
|
+
*/
|
|
8
|
+
import type { Branded } from '@hasna-internal/kai-brand';
|
|
9
|
+
import type { SessionId } from '@hasna-internal/kai-session/types';
|
|
10
|
+
/** Identifies one workflow run. */
|
|
11
|
+
export type WorkflowRunId = Branded<'WorkflowRunId'>;
|
|
12
|
+
/**
|
|
13
|
+
* Brand a string as a {@link WorkflowRunId}.
|
|
14
|
+
* @param id - the raw id string (the engine mints UUIDs; tests may pass fixtures).
|
|
15
|
+
* @returns the same string, branded.
|
|
16
|
+
*/
|
|
17
|
+
export declare function WorkflowRunId(id: string): WorkflowRunId;
|
|
18
|
+
/**
|
|
19
|
+
* One phase declared in a script's `meta.phases` (progress vocabulary only —
|
|
20
|
+
* phases group agents in observers/UIs; they impose no execution structure).
|
|
21
|
+
*/
|
|
22
|
+
export interface WorkflowPhase {
|
|
23
|
+
/** The phase title; `phase()` calls match against it by exact string. */
|
|
24
|
+
title: string;
|
|
25
|
+
/** Optional one-line description of what the phase does. */
|
|
26
|
+
detail?: string;
|
|
27
|
+
/** Optional provider override this phase is expected to use (informational). */
|
|
28
|
+
provider?: string;
|
|
29
|
+
/** Optional model override this phase is expected to use (informational). */
|
|
30
|
+
model?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The script's identity block, provided as plain JSON data alongside the
|
|
34
|
+
* script body (the model-facing tool carries it as its `meta` parameter) and
|
|
35
|
+
* validated by the engine before the body runs. `name`/`description` are
|
|
36
|
+
* required; the rest is optional annotation. The field vocabulary matches the
|
|
37
|
+
* Claude Code dynamic-workflows meta block.
|
|
38
|
+
*/
|
|
39
|
+
export interface WorkflowMeta {
|
|
40
|
+
/** Short kebab-case workflow name (display + persistence key). */
|
|
41
|
+
name: string;
|
|
42
|
+
/** One-line description of what the workflow does. */
|
|
43
|
+
description: string;
|
|
44
|
+
/** Optional guidance on when this workflow applies (shown in listings). */
|
|
45
|
+
whenToUse?: string;
|
|
46
|
+
/** Optional phase declarations matched by `phase()` calls. */
|
|
47
|
+
phases?: WorkflowPhase[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Why a run settled. CLOSED union (engine-owned, consumers may exhaust):
|
|
51
|
+
* `completed` = the script ran to its final `return`; `cancelled` = the run
|
|
52
|
+
* was cancelled (caller `cancel()`/signal); `error` = the script threw, a
|
|
53
|
+
* fatal `WorkflowError` propagated, or the result failed materialization.
|
|
54
|
+
*/
|
|
55
|
+
export type WorkflowStopReason = 'completed' | 'cancelled' | 'error';
|
|
56
|
+
/**
|
|
57
|
+
* The outcome resolved by a live workflow run. `value` is
|
|
58
|
+
* the script's materialized return value (plain host-realm JSON data; `null`
|
|
59
|
+
* when the script returned `undefined`) — meaningful only for `completed`.
|
|
60
|
+
* A non-`completed` reason carries the failure in `error`; the consumer maps
|
|
61
|
+
* it to an `isError` tool result rather than reporting partial output.
|
|
62
|
+
*/
|
|
63
|
+
export interface WorkflowResult {
|
|
64
|
+
/** The script's return value (host JSON data; `null` for no return). */
|
|
65
|
+
value: unknown;
|
|
66
|
+
/** Why the run settled. */
|
|
67
|
+
stopReason: WorkflowStopReason;
|
|
68
|
+
/** The failure message (present iff `stopReason` is not `completed`). */
|
|
69
|
+
error?: string;
|
|
70
|
+
/**
|
|
71
|
+
* How many `agent()` calls the run accepted over its whole lifetime. On a
|
|
72
|
+
* graceful settlement this is the script-side count (calls still queued for
|
|
73
|
+
* a concurrency slot included); on a termination path (grace force-settle,
|
|
74
|
+
* worker death) it degrades to the host-observed count — calls queued
|
|
75
|
+
* inside a terminated script are unknowable then.
|
|
76
|
+
*/
|
|
77
|
+
agentsStarted: number;
|
|
78
|
+
}
|
|
79
|
+
/** Identifying detail for a run, carried by every `workflow/*` event as borrowed immutable data, never the live run. */
|
|
80
|
+
export interface WorkflowRunInfo {
|
|
81
|
+
/** The run's id. */
|
|
82
|
+
id: WorkflowRunId;
|
|
83
|
+
/** The run's validated meta block. */
|
|
84
|
+
meta: WorkflowMeta;
|
|
85
|
+
}
|
|
86
|
+
/** One `agent()` call's identity within a run (the `workflow/agent-start` payload). */
|
|
87
|
+
export interface WorkflowAgentInfo {
|
|
88
|
+
/** 1-based sequence number of this `agent()` call within the run. */
|
|
89
|
+
seq: number;
|
|
90
|
+
/** The display label (the `label` option, or a prompt snippet). */
|
|
91
|
+
label: string;
|
|
92
|
+
/** The phase this agent belongs to (the `phase` option, else the current `phase()` title). */
|
|
93
|
+
phase?: string;
|
|
94
|
+
/** The child agent's id on the subagent seam. */
|
|
95
|
+
childId: SessionId;
|
|
96
|
+
}
|
|
97
|
+
/** How one `agent()` call settled: clean result, child failure (script sees `null`), or run cancellation. */
|
|
98
|
+
export type WorkflowAgentOutcome = 'completed' | 'failed' | 'cancelled';
|
|
99
|
+
/** One `agent()` call's settlement (the `workflow/agent-end` payload). */
|
|
100
|
+
export interface WorkflowAgentEndInfo extends WorkflowAgentInfo {
|
|
101
|
+
/** How the call settled. */
|
|
102
|
+
outcome: WorkflowAgentOutcome;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A settled run's outcome as event data (the `workflow/end` payload): the
|
|
106
|
+
* {@link WorkflowResult} minus `value` (a listener observing outcomes must not
|
|
107
|
+
* receive a mutable alias of the caller's result value; a consumer that needs
|
|
108
|
+
* the value holds the run and awaits `result`).
|
|
109
|
+
*/
|
|
110
|
+
export interface WorkflowResultInfo {
|
|
111
|
+
/** Why the run settled. */
|
|
112
|
+
stopReason: WorkflowStopReason;
|
|
113
|
+
/** The failure message (present iff `stopReason` is not `completed`). */
|
|
114
|
+
error?: string;
|
|
115
|
+
/** How many `agent()` calls the run accepted (see {@link WorkflowResult.agentsStarted}). */
|
|
116
|
+
agentsStarted: number;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow seam vocabulary: the request/run/result types a workflow engine
|
|
3
|
+
* consumes and produces, plus the fields in the `workflow/*` event payloads.
|
|
4
|
+
* Types only (plus the id-brand factory), per the package convention.
|
|
5
|
+
*
|
|
6
|
+
* @module @hasna-internal/kai-workflow/types
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Brand a string as a {@link WorkflowRunId}.
|
|
10
|
+
* @param id - the raw id string (the engine mints UUIDs; tests may pass fixtures).
|
|
11
|
+
* @returns the same string, branded.
|
|
12
|
+
*/
|
|
13
|
+
export function WorkflowRunId(id) {
|
|
14
|
+
return id;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasna-internal/kai-workflow",
|
|
3
|
+
"description": "Workflow capability seam: ctx.workflowEngine service, run vocabulary, and workflow/* events",
|
|
4
|
+
"version": "0.1.1-rc.2",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/workflow/workflow"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./types": {
|
|
26
|
+
"types": "./lib/types/types.d.ts",
|
|
27
|
+
"default": "./lib/types/types.js"
|
|
28
|
+
},
|
|
29
|
+
"./src/*": "./src/*",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"lib/index.js",
|
|
34
|
+
"lib/invariant.js",
|
|
35
|
+
"lib/types/**/*.js",
|
|
36
|
+
"lib/types/**/*.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@hasna-internal/kai-invariants": "^0.1.1-rc.2",
|
|
41
|
+
"@hasna-internal/kai-session": "^0.1.1-rc.2",
|
|
42
|
+
"@hasna-internal/kai-agent": "^0.1.1-rc.2",
|
|
43
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
44
|
+
"@hasna-internal/kai-llm": "^0.1.1-rc.2",
|
|
45
|
+
"@hasna-internal/kai-brand": "^0.1.1-rc.2"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@hasna-internal/kai-agent": "^0.1.1-rc.2",
|
|
49
|
+
"@hasna-internal/kai-brand": "^0.1.1-rc.2",
|
|
50
|
+
"@hasna-internal/kai-invariants": "^0.1.1-rc.2",
|
|
51
|
+
"@hasna-internal/kai-session": "^0.1.1-rc.2",
|
|
52
|
+
"@hasna-internal/kai-llm": "^0.1.1-rc.2",
|
|
53
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
54
|
+
}
|
|
55
|
+
}
|