@zhushanwen/pi-subagent-workflow 8.3.0 → 8.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +18 -4
- package/relay/relay.mjs +390 -0
- package/skills/subagent-ext-config/SKILL.md +80 -0
- package/src/execution/__tests__/agent-registry.test.ts +110 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +597 -0
- package/src/execution/__tests__/execution-record.test.ts +127 -1
- package/src/execution/__tests__/pi-invocation.test.ts +62 -1
- package/src/execution/__tests__/relay-agent.test.ts +448 -0
- package/src/execution/__tests__/relay-env.test.ts +42 -0
- package/src/execution/__tests__/startup-config-declaration.test.ts +35 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +53 -5
- package/src/execution/agent-registry.ts +10 -0
- package/src/execution/config.ts +25 -2
- package/src/execution/engine/__tests__/common/data-dir.test.ts +53 -0
- package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
- package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
- package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
- package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
- package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +342 -0
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
- package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
- package/src/execution/engine/__tests__/engines-declaration.test.ts +36 -0
- package/src/execution/engine/__tests__/model-prompt.test.ts +85 -0
- package/src/execution/engine/__tests__/paths.test.ts +39 -0
- package/src/execution/engine/__tests__/registry.test.ts +120 -0
- package/src/execution/engine/__tests__/routing.test.ts +231 -0
- package/src/execution/engine/common/data-dir.ts +62 -0
- package/src/execution/engine/common/errors.ts +183 -0
- package/src/execution/engine/common/event-journal.ts +254 -0
- package/src/execution/engine/common/journal-replay.ts +62 -0
- package/src/execution/engine/common/kill-chain.ts +221 -0
- package/src/execution/engine/common/nesting-guard.ts +50 -0
- package/src/execution/engine/common/persona-router.ts +108 -0
- package/src/execution/engine/common/pool-manager.ts +226 -0
- package/src/execution/engine/common/schema-emulation.ts +189 -0
- package/src/execution/engine/common/session-view-projection.ts +51 -0
- package/src/execution/engine/engine-discovery.ts +65 -0
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
- package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
- package/src/execution/engine/engines/pi/reader.ts +48 -0
- package/src/execution/engine/engines/pi/registration.ts +35 -0
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +64 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +567 -0
- package/src/execution/engine/engines/zcode/constants.ts +43 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
- package/src/execution/engine/engines/zcode/launcher.ts +161 -0
- package/src/execution/engine/engines/zcode/parser.ts +436 -0
- package/src/execution/engine/engines/zcode/preparer.ts +363 -0
- package/src/execution/engine/engines/zcode/reader.ts +381 -0
- package/src/execution/engine/engines/zcode/registration.ts +37 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +648 -0
- package/src/execution/engine/host-task-spec.ts +47 -0
- package/src/execution/engine/model-prompt.ts +59 -0
- package/src/execution/engine/paths.ts +42 -0
- package/src/execution/engine/port.ts +153 -0
- package/src/execution/engine/registry.ts +123 -0
- package/src/execution/engine/routing.ts +218 -0
- package/src/execution/engine/types.ts +304 -0
- package/src/execution/execute-options-mapper.ts +5 -1
- package/src/execution/execution-record.ts +6 -0
- package/src/execution/model-resolver.ts +6 -0
- package/src/execution/pi-invocation.ts +32 -2
- package/src/execution/record-entry.ts +14 -0
- package/src/execution/record-store.ts +34 -0
- package/src/execution/relay-env.ts +37 -0
- package/src/execution/session-runner.ts +24 -0
- package/src/execution/stream-sink.ts +26 -0
- package/src/execution/subagent-service.ts +249 -11
- package/src/execution/subprocess-agent-runner.ts +196 -14
- package/src/execution/types.ts +56 -0
- package/src/index.ts +46 -1
- package/src/interface/command-actions.ts +72 -8
- package/src/interface/subagent-actions.ts +8 -2
- package/src/interface/subagent-tool.ts +6 -0
- package/src/interface/subagents.ts +198 -30
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +5 -2
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +3 -3
- package/src/orchestration/models/types.ts +7 -0
- package/src/orchestration/worker-script-builder.ts +5 -2
- package/src/shared/meta-parser.ts +5 -1
- package/src/shared/resource-meta.ts +5 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/execution/engine/host-task-spec.ts
|
|
2
|
+
//
|
|
3
|
+
// ExecuteOptions → AgentTaskSpec 的宿主侧最小正向映射(U0:chat 工具域引擎分支)。
|
|
4
|
+
//
|
|
5
|
+
// 与 engines/pi/task-spec-mapper.ts 的关系:那是 pi 引擎方向的映射(taskSpecToExecuteOptions
|
|
6
|
+
// 还原 + schemaEnv 内化派生等 pi 专有语义),归属 pi 引擎模块;本 mapper 服务宿主编排层
|
|
7
|
+
// (subagent-service 引擎分支),不 import 任何具体引擎模块(registry.ts 依赖方向纪律:
|
|
8
|
+
// 上层按中立类型组装声明,引擎细节归引擎)。字段映射是 ExecuteOptions → AgentTaskSpec
|
|
9
|
+
// 的中立直译(task/slug/agent/model 原样;thinkingLevel → effort;skillPath +
|
|
10
|
+
// appendSystemPrompt → persona;schema/maxTurns/graceTurns/worktree/cwd 原样透传)。
|
|
11
|
+
//
|
|
12
|
+
// conversation/fork 在调用本 mapper 前已被 assertEngineParamSupport 预检拒绝(非 pi 引擎
|
|
13
|
+
// unsupported),透传仅保持映射完整性——未来引擎支持时无需改本函数。
|
|
14
|
+
|
|
15
|
+
import type { ExecuteOptions } from "../types.ts";
|
|
16
|
+
import type { AgentTaskSpec, PersonaSpec } from "./types.ts";
|
|
17
|
+
|
|
18
|
+
/** 纯函数:ExecuteOptions(chat 域执行选项)→ 引擎中立任务声明。无副作用、幂等。 */
|
|
19
|
+
export function executeOptionsToEngineTaskSpec(opts: ExecuteOptions): AgentTaskSpec {
|
|
20
|
+
// persona 归并口径与 pi mapper 一致:skillPath / appendSystemPrompt 有其一即建,
|
|
21
|
+
// 避免空对象噪声(下游 spread 空数组与 undefined 均为 no-op)。
|
|
22
|
+
const persona: PersonaSpec | undefined =
|
|
23
|
+
opts.skillPath !== undefined || opts.appendSystemPrompt !== undefined
|
|
24
|
+
? {
|
|
25
|
+
...(opts.skillPath !== undefined ? { skillPath: opts.skillPath } : {}),
|
|
26
|
+
...(opts.appendSystemPrompt !== undefined ? { appendSystemPrompt: opts.appendSystemPrompt } : {}),
|
|
27
|
+
}
|
|
28
|
+
: undefined;
|
|
29
|
+
return {
|
|
30
|
+
task: opts.task,
|
|
31
|
+
slug: opts.slug,
|
|
32
|
+
agent: opts.agent,
|
|
33
|
+
model: opts.model,
|
|
34
|
+
// pi 7 档 thinkingLevel 是引擎私有语义——中立层透传 effort 字符串,各引擎自行映射
|
|
35
|
+
effort: opts.thinkingLevel,
|
|
36
|
+
...(persona !== undefined ? { persona } : {}),
|
|
37
|
+
schema: opts.schema,
|
|
38
|
+
maxTurns: opts.maxTurns,
|
|
39
|
+
graceTurns: opts.graceTurns,
|
|
40
|
+
fork: opts.fork,
|
|
41
|
+
worktree: opts.worktree,
|
|
42
|
+
cwd: opts.cwd,
|
|
43
|
+
conversation: opts.conversation,
|
|
44
|
+
idleTimeoutMs: opts.idleTimeoutMs,
|
|
45
|
+
// 运行期字段(signal/ctxModel)不入声明——归 RunContext(port.ts 删字段去向)。
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/execution/engine/model-prompt.ts
|
|
2
|
+
//
|
|
3
|
+
// [U7] 引擎模型段 system prompt 注入(模型可发现性)。
|
|
4
|
+
//
|
|
5
|
+
// 背景:pi 核心在 system prompt 生成 <available_provider_models> 段并教育 agent
|
|
6
|
+
// 「subagent/workflow 的 model 参数用这些 id」——该教育对 engine: zcode 等自带
|
|
7
|
+
// provider 体系的引擎是误导(pi 的 id 在引擎侧是未知 provider)。本模块按
|
|
8
|
+
// 「defaultEngine 开关」(用户拍板 2026-08-25)补齐非默认引擎的模型可发现性:
|
|
9
|
+
// defaultEngine 指向非 pi 引擎且该引擎实现 EnginePort.listModels 时,追加
|
|
10
|
+
// <available_<engine>_models> 段,与 pi 段形成「一段一引擎」的分界标注。
|
|
11
|
+
//
|
|
12
|
+
// engine-neutral:遍历注册表驱动,未来引擎实现 listModels 即自动注入,宿主零改动。
|
|
13
|
+
|
|
14
|
+
import { DEFAULT_ENGINE_ID, getEngine, hasEngine } from "./registry.ts";
|
|
15
|
+
|
|
16
|
+
/** pi 段之外的引擎才需要清单段(pi 与主 agent 模型体系一致,核心已有段)。 */
|
|
17
|
+
const CORE_ENGINE_ID = DEFAULT_ENGINE_ID;
|
|
18
|
+
|
|
19
|
+
/** 单引擎段格式:XML 包裹 + 分界语义(与 pi 核心段的 <model><id> 形态对齐)。 */
|
|
20
|
+
function buildEngineSection(engineId: string, models: Array<{ id: string; name?: string }>): string {
|
|
21
|
+
const lines = models.map((m) =>
|
|
22
|
+
m.name !== undefined ? `<model><id>${m.id}</id><name>${m.name}</name></model>` : `<model><id>${m.id}</id></model>`,
|
|
23
|
+
);
|
|
24
|
+
return [
|
|
25
|
+
`<available_${engineId}_models>`,
|
|
26
|
+
`The following models are available for subagents dispatched with engine '${engineId}' ONLY (their provider registry differs from the main agent's; ids above in <available_provider_models> do NOT apply to engine '${engineId}' dispatches). Omit model to use the engine default.`,
|
|
27
|
+
...lines,
|
|
28
|
+
`</available_${engineId}_models>`,
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 依据全局 defaultEngine 生成追加段(不含前导换行;无需注入时返回空串)。
|
|
34
|
+
*
|
|
35
|
+
* 规则(用户拍板的开关语义):
|
|
36
|
+
* - defaultEngine 为 pi / 缺省 → 不注入(pi 段已由核心提供);
|
|
37
|
+
* - defaultEngine 指向已注册且实现 listModels 的引擎 → 注入该引擎段;
|
|
38
|
+
* - 引擎未实现 listModels / 清单为空 → 不注入(可发现性降级不阻塞,prepare 报错
|
|
39
|
+
* 的事后清单兜底仍在)。
|
|
40
|
+
* fail-safe:任何异常返回空串(注入失败不阻塞 agent loop——与 system-prompt
|
|
41
|
+
* extension 的 before_agent_start 处置一致)。
|
|
42
|
+
*/
|
|
43
|
+
export function buildEngineModelsPromptAppend(defaultEngine: string | undefined): string {
|
|
44
|
+
try {
|
|
45
|
+
const engineId = defaultEngine?.trim() || CORE_ENGINE_ID;
|
|
46
|
+
if (engineId === CORE_ENGINE_ID) return "";
|
|
47
|
+
if (!hasEngine(engineId)) return "";
|
|
48
|
+
const engine = getEngine(engineId);
|
|
49
|
+
if (engine.listModels === undefined) return "";
|
|
50
|
+
const models = engine.listModels();
|
|
51
|
+
if (models === null || models.length === 0) return "";
|
|
52
|
+
return buildEngineSection(engineId, models);
|
|
53
|
+
} catch {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 测试口:段格式导出(避免测试依赖注册表组装全链)。 */
|
|
59
|
+
export const _testHooks = { buildEngineSection, CORE_ENGINE_ID };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 引擎数据目录布局 SSOT(设计文档 D5/D6)。
|
|
5
|
+
*
|
|
6
|
+
* 为什么独立成模块:extension 写侧(journal 落盘 / preparer 池目录)与
|
|
7
|
+
* runtime 校验侧(subagent-extractor 前缀白名单)必须同源推导——
|
|
8
|
+
* 双方 import 同一份纯函数,禁止各自拼字符串漂移。
|
|
9
|
+
*
|
|
10
|
+
* 布局:`<dataDir>/engines/<engineId>/<pool-key>/journal-<taskId>.jsonl`
|
|
11
|
+
* 隔离池跨任务保留复用;journal 生命周期跟随 record,不随池删除(D5)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** 路径段安全编码后的最大字符数(防超长段击穿文件名长度上限;runtime 校验侧同源)。 */
|
|
15
|
+
const MAX_SEG_CHARS = 80;
|
|
16
|
+
|
|
17
|
+
/** 路径段进入文件系统前的安全编码:路径穿越、分隔符、空白、超长全部归一。 */
|
|
18
|
+
export function sanitizeSeg(input: string): string {
|
|
19
|
+
const s = input.replace(/[^A-Za-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
20
|
+
return s.length > 0 ? s.slice(0, MAX_SEG_CHARS) : 'default';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveEnginesRoot(dataDir: string): string {
|
|
24
|
+
return join(dataDir, 'engines');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveEngineDir(dataDir: string, engineId: string): string {
|
|
28
|
+
return join(resolveEnginesRoot(dataDir), sanitizeSeg(engineId));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolvePoolDir(dataDir: string, engineId: string, poolKey: string): string {
|
|
32
|
+
return join(resolveEngineDir(dataDir, sanitizeSeg(engineId)), sanitizeSeg(poolKey));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveJournalPath(
|
|
36
|
+
dataDir: string,
|
|
37
|
+
engineId: string,
|
|
38
|
+
poolKey: string,
|
|
39
|
+
taskId: string,
|
|
40
|
+
): string {
|
|
41
|
+
return join(resolvePoolDir(dataDir, engineId, poolKey), `journal-${sanitizeSeg(taskId)}.jsonl`);
|
|
42
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// src/execution/engine/port.ts
|
|
2
|
+
//
|
|
3
|
+
// EnginePort 接口(P1)。设计权威源:docs/architecture/subagent-engine-abstraction.md
|
|
4
|
+
// §3.3.5「EnginePort 完整签名」——本文件是可编码落地的契约层,后续 wave(公共降级层
|
|
5
|
+
// P2 / zcode 引擎 P3 / 配置路由 P4)以本接口为实现契约,字段级变更须先改设计文档。
|
|
6
|
+
//
|
|
7
|
+
// 四个能力面(D1):
|
|
8
|
+
// run —— 主语义:一次性 fire-to-completion 任务执行;
|
|
9
|
+
// interact —— 交互控制面(chatMode 的 message/close/cancel + idle,可选能力面,
|
|
10
|
+
// capabilities.conversation 声明接通与否);
|
|
11
|
+
// read —— session 历史读取(D6 三级降级链);
|
|
12
|
+
// probe —— 探针(D7:二进制存在/版本解析/干跑校验)。
|
|
13
|
+
// capabilities() 同步无副作用——「调用前拒绝」(D11 处置三级)的判据。
|
|
14
|
+
|
|
15
|
+
import type { ChildProcess } from "node:child_process";
|
|
16
|
+
|
|
17
|
+
import type { ModelInfo } from "../model-resolver.ts";
|
|
18
|
+
import type { SubagentStream } from "../stream-sink.ts";
|
|
19
|
+
import type { AgentEvent } from "../types.ts";
|
|
20
|
+
import type {
|
|
21
|
+
AgentOutcome,
|
|
22
|
+
AgentTaskSpec,
|
|
23
|
+
EngineCapabilities,
|
|
24
|
+
EngineHandle,
|
|
25
|
+
InteractAction,
|
|
26
|
+
InteractResult,
|
|
27
|
+
ProbeReport,
|
|
28
|
+
SessionView,
|
|
29
|
+
} from "./types.ts";
|
|
30
|
+
|
|
31
|
+
// ============================================================
|
|
32
|
+
// RunContext(run 的运行期上下文)
|
|
33
|
+
// ============================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* run 的运行期上下文。任务声明(AgentTaskSpec)与运行期句柄分离——signal/ctxModel/
|
|
37
|
+
* onComplete 从 ExecuteOptions 移出(设计 §3.3.5 删字段去向),因为它们是宿主注入的
|
|
38
|
+
* 运行期对象,不属于跨引擎持久化的任务声明。
|
|
39
|
+
*
|
|
40
|
+
* 常驻进程友好(D1):onEvent 回调式(而非迭代器式)+ AbortSignal——引擎内部换常驻
|
|
41
|
+
* server 实现(未来 driver host)时接口不动。
|
|
42
|
+
*/
|
|
43
|
+
export interface RunContext {
|
|
44
|
+
/** = record.id(bg-N-xxx / run-N)——journal 文件名与池引用计数 key(P2 消费)。 */
|
|
45
|
+
taskId: string;
|
|
46
|
+
/** D5 隔离池(宿主分配,设计 §3.3.9;pi 无池化恒 'shared')。 */
|
|
47
|
+
poolKey: string;
|
|
48
|
+
/** abort 分级入口(D1:引擎原生中断 → 公共杀链兜底)。 */
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
/** 事件流出口(host 消费后统一落 journal,D6 第②级)。 */
|
|
51
|
+
onEvent?: (event: AgentEvent) => void;
|
|
52
|
+
/** model 解析第三层兼底(现有 D-008 语义不变)。 */
|
|
53
|
+
ctxModel?: ModelInfo;
|
|
54
|
+
/**
|
|
55
|
+
* text_delta streaming 通道(宿主侧 UI widget)。与 onEvent 平行的 text_delta 出口:
|
|
56
|
+
* background 路径 onEvent=undefined 但流式仍需送达(双通道互斥设计,见 session-runner
|
|
57
|
+
* agentEvent 出口注释)。pi 回填期承载 AgentRunner port 的 stream 透传(行为零变化),
|
|
58
|
+
* 语义上是宿主设施而非引擎专有——未来引擎的 text_delta 同样可走此通道。
|
|
59
|
+
*/
|
|
60
|
+
stream?: SubagentStream;
|
|
61
|
+
/**
|
|
62
|
+
* [P1 pi 回填透传] 调用方已持有的 schema 激活预编码值(AgentCallOpts.schemaEnv 直传
|
|
63
|
+
* 形态)。生产路径中 resolveAgentOpts 恒耦合产出 schema+schemaEnv(值 = JSON.stringify
|
|
64
|
+
* (schema)),引擎从 task.schema 派生即可逐字节等值;解耦形态(有 schemaEnv 无
|
|
65
|
+
* schema)生产不可达、仅见于直构调用,派生无源——本字段是其唯一透交通道。
|
|
66
|
+
* 引擎在 task.schema 存在时忽略此值(派生优先,设计 §3.3.5 删字段去向)。
|
|
67
|
+
*/
|
|
68
|
+
schemaEnv?: string;
|
|
69
|
+
/**
|
|
70
|
+
* [P4 D9①] 引擎 fallback 留痕(probe 失败路由回默认引擎)。路由层(routing.ts)
|
|
71
|
+
* 产出,引擎投影到 outcome.engineFallback(zcode 等无 record 通路的引擎以此留痕;
|
|
72
|
+
* pi 引擎另经 ExecuteOptions 投影进 record)。
|
|
73
|
+
*/
|
|
74
|
+
engineFallback?: { from: string; reason: string };
|
|
75
|
+
/**
|
|
76
|
+
* [P4 对齐点③] 引擎声明实际隔离池 key(journal 落盘路径权威)。宿主创建 journal
|
|
77
|
+
* writer 时只能用缺省占位 poolKey(pi 恒 'shared'),非池化稳定的引擎(zcode 按
|
|
78
|
+
* provider+model 池化)在 prepare 期确定 poolKey 后回调本方法重定向 writer——
|
|
79
|
+
* 保证 journal 落盘路径与 handle.poolKey 同源(单一权威,不再两边推导)。
|
|
80
|
+
* 契约:必须在首个事件 emit 之前调用(zcode coarse 事件在终态后合成,天然满足;
|
|
81
|
+
* 未来流式引擎需在事件出口前调用)。
|
|
82
|
+
*/
|
|
83
|
+
onPoolResolved?: (poolKey: string) => void;
|
|
84
|
+
/**
|
|
85
|
+
* [U0 D10] 引擎 spawn 的子进程句柄注册钩子(宿主终止链记账)。引擎在 spawn 成功后
|
|
86
|
+
* 同步回调(与 pi runSpawn 的 spawnedChildren.set 同构时机);宿主据此把 child 注册进
|
|
87
|
+
* session-runner 的 spawnedChildren Map(cancel SIGTERM / dispose 收割兜底 / killAll
|
|
88
|
+
* 全量清理对非 pi 引擎 record 生效)。close/error 后由宿主按句守卫移除。可选:引擎
|
|
89
|
+
* 内部不 spawn 进程(如未来常驻 driver host 实现)时不调用,宿主记账自然为空。
|
|
90
|
+
*/
|
|
91
|
+
onChildSpawned?: (child: ChildProcess) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============================================================
|
|
95
|
+
// run 返回(handle + outcome)
|
|
96
|
+
// ============================================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* run 的返回:终态 + 可持久化 handle。
|
|
100
|
+
*
|
|
101
|
+
* handle 语义(设计 §3.3.5 run 错误语义三条):prepare 期错误(credential_missing /
|
|
102
|
+
* model_not_available / prompt_too_large)在进程创建前 reject、不产生 handle;运行中
|
|
103
|
+
* 失败不 reject——合成 error outcome + 正常 handle 返回(record 必须收尾);abort 走
|
|
104
|
+
* 完杀链后同前(exitCode=null + error 含杀链标记)。
|
|
105
|
+
*/
|
|
106
|
+
export interface EngineRunResult {
|
|
107
|
+
handle: EngineHandle;
|
|
108
|
+
outcome: AgentOutcome;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================
|
|
112
|
+
// EnginePort
|
|
113
|
+
// ============================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* subagent 执行引擎的唯一契约点(D1)。实现方:PiEngine(回填)/ ZcodeEngine(P3)/
|
|
117
|
+
* 未来各引擎适配器。上层(工具面/workflow 引擎/GUI)只消费中立类型,不感知引擎。
|
|
118
|
+
*
|
|
119
|
+
* 贯穿纪律(设计 §3.3.1):宿主编排——引擎只当单 agent 执行器,六家原生多 agent 机制
|
|
120
|
+
* 一律禁用不依赖。
|
|
121
|
+
*/
|
|
122
|
+
export interface EnginePort {
|
|
123
|
+
/** 注册表 key('pi' | 'zcode' | ...)。 */
|
|
124
|
+
readonly id: string;
|
|
125
|
+
|
|
126
|
+
/** D3(同步无副作用——调用前拒绝的判据)。 */
|
|
127
|
+
capabilities(): EngineCapabilities;
|
|
128
|
+
|
|
129
|
+
/** D7(factory 初始化 + 版本变化检测触发;opts.force 跳过缓存强探)。 */
|
|
130
|
+
probe(opts?: { force?: boolean }): Promise<ProbeReport>;
|
|
131
|
+
|
|
132
|
+
/** D1 主语义:fire-to-completion。 */
|
|
133
|
+
run(task: AgentTaskSpec, ctx: RunContext): Promise<EngineRunResult>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* D1 可选面:交互控制面。pi 首期原生实现(现有 chatMode 行为直通);不支持
|
|
137
|
+
* conversation 的引擎返回 engine_capability_unsupported(同步拒绝、不创建进程)。
|
|
138
|
+
*/
|
|
139
|
+
interact(handle: EngineHandle, action: InteractAction): Promise<InteractResult>;
|
|
140
|
+
|
|
141
|
+
/** D6 三级降级链:①引擎原生读取 → ②宿主 event journal(P2)→ ③outcome-only。 */
|
|
142
|
+
read(handle: EngineHandle): Promise<SessionView>;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* [U7] 可选面:模型可发现性——引擎自带 provider/model 体系时(如 zcode 的 v2 桌面
|
|
146
|
+
* 登录态),列出当前环境实际可用的模型清单(带凭据校验),供 system prompt 引擎段
|
|
147
|
+
* 与 GUI 引擎选择器消费。省略/返回 null = 「与主 agent 模型体系一致」(pi 的语义:
|
|
148
|
+
* system prompt 已有 <available_provider_models> 段,无需引擎再列)。
|
|
149
|
+
* engine-neutral:未来引擎(AcpEngine 等)实现本方法即自动获得注入与展示,宿主
|
|
150
|
+
* 侧零改动。
|
|
151
|
+
*/
|
|
152
|
+
listModels?(): Array<{ id: string; name?: string }> | null;
|
|
153
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/execution/engine/registry.ts
|
|
2
|
+
//
|
|
3
|
+
// 引擎注册表(P1)。设计权威源:docs/architecture/subagent-engine-abstraction.md
|
|
4
|
+
// §3.3.1 分层总图([引擎注册表 engine registry](id → factory))与 §3.3.3 错误规格
|
|
5
|
+
// 第 1 行(engine_not_found → 指向注册表清单 + 配置文件路径)。
|
|
6
|
+
//
|
|
7
|
+
// 为什么需要注册表:引擎身份是「spawn 细节的归属边界」(设计 §2.3 根因一)——上层
|
|
8
|
+
// (配置路由/agent 解析,P4 接线)按 id 取引擎,不感知实现类;新引擎接入 = 新增一个
|
|
9
|
+
// 适配器模块 + 注册表登记一行,不改上层与既有引擎(目标 5)。
|
|
10
|
+
//
|
|
11
|
+
// 依赖方向(设计 §3.3.1 贯穿纪律④):registry 只依赖 port.ts 的类型,不 import 任何
|
|
12
|
+
// 具体引擎——注册由组合根(index.ts)或引擎自己的 registration 模块完成,防循环依赖。
|
|
13
|
+
|
|
14
|
+
import type { EnginePort } from "./port.ts";
|
|
15
|
+
|
|
16
|
+
/** 引擎工厂:惰性创建引擎实例(getEngine 首次取用时执行)。 */
|
|
17
|
+
export type EngineFactory = () => EnginePort;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 缺省引擎 id(D9:缺省引擎 = 'pi',回填期零风险默认)。
|
|
21
|
+
* P1 无 per-agent/调用级 engine 字段(P4 配置路由引入三层优先级),一切执行恒走缺省。
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_ENGINE_ID = "pi";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* engine_not_found(错误规格表第 1 行):agent frontmatter 写了未注册 engine id。
|
|
27
|
+
* 错误文案契约:含已注册引擎清单 + 配置文件指引——配置错误前置暴露(agent 解析期),
|
|
28
|
+
* 不留到运行时神秘失败(设计目标 4)。
|
|
29
|
+
*/
|
|
30
|
+
export class EngineNotFoundError extends Error {
|
|
31
|
+
/** 结构化错误码(§3.3.3 错误规格表的 code 列,供调用方程序化分流)。 */
|
|
32
|
+
readonly code = "engine_not_found";
|
|
33
|
+
/** 请求的(未注册的)引擎 id。 */
|
|
34
|
+
readonly engineId: string;
|
|
35
|
+
/** 请求时刻的已注册清单快照(防错误对象跨时间读 Map 的失真)。 */
|
|
36
|
+
readonly registered: readonly string[];
|
|
37
|
+
/** 错误来源定位(agent .md 文件路径 / 配置键等;运行期 getEngine 无来源不传)。 */
|
|
38
|
+
readonly source: string | undefined;
|
|
39
|
+
|
|
40
|
+
constructor(engineId: string, registered: readonly string[], source?: string) {
|
|
41
|
+
super(
|
|
42
|
+
`engine_not_found: engine '${engineId}' is not registered. ` +
|
|
43
|
+
`Registered engines: ${registered.length > 0 ? registered.join(", ") : "(none)"}. ` +
|
|
44
|
+
`Recovery: check the engine id in the agent .md frontmatter (engine: field) or the global ` +
|
|
45
|
+
`default engine setting, fix the typo, or install/register the engine first ` +
|
|
46
|
+
`(registered engines are listed above).` +
|
|
47
|
+
(source !== undefined ? ` Source: ${source}.` : ""),
|
|
48
|
+
);
|
|
49
|
+
this.name = "EngineNotFoundError";
|
|
50
|
+
this.engineId = engineId;
|
|
51
|
+
this.registered = registered;
|
|
52
|
+
this.source = source;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* id → factory(注册表本体)+ id → 惰性单例(getEngine 首次取用创建;registerEngine
|
|
58
|
+
* 覆盖时丢弃旧实例)。进程级单例状态,用 globalThis[Symbol.for] 持有防 jiti 双路径
|
|
59
|
+
* 加载分裂(development-guide §7.5),不用模块级 const。
|
|
60
|
+
*/
|
|
61
|
+
const ENGINE_REGISTRY_SLOT_KEY = Symbol.for("@zhushanwen/pi-subagent-workflow.engineRegistry");
|
|
62
|
+
|
|
63
|
+
/** 注册表槽位形状(同文件唯一写入点,运行时保证)。 */
|
|
64
|
+
interface EngineRegistrySlot {
|
|
65
|
+
factories: Map<string, EngineFactory>;
|
|
66
|
+
singletons: Map<string, EnginePort>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getRegistrySlot(): EngineRegistrySlot {
|
|
70
|
+
// globalThis 无 symbol 索引签名,但运行时支持 symbol 键——用 Reflect 安全读写,
|
|
71
|
+
// 避免双重断言(同 model-config-service.ts 先例)。
|
|
72
|
+
let slot = Reflect.get(globalThis, ENGINE_REGISTRY_SLOT_KEY) as EngineRegistrySlot | undefined;
|
|
73
|
+
if (!slot) {
|
|
74
|
+
slot = { factories: new Map(), singletons: new Map() };
|
|
75
|
+
Reflect.set(globalThis, ENGINE_REGISTRY_SLOT_KEY, slot);
|
|
76
|
+
}
|
|
77
|
+
return slot;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 登记引擎工厂。重复注册同一 id = 覆盖(组合根可能多次执行,如每次 session_start 重跑
|
|
82
|
+
* registerPiEngine——幂等覆盖保证不炸也不堆积),覆盖时丢弃缓存的旧单例,让下一次
|
|
83
|
+
* getEngine 用新工厂重建。
|
|
84
|
+
*/
|
|
85
|
+
export function registerEngine(id: string, factory: EngineFactory): void {
|
|
86
|
+
const slot = getRegistrySlot();
|
|
87
|
+
slot.factories.set(id, factory);
|
|
88
|
+
slot.singletons.delete(id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 未注册 id 抛 EngineNotFoundError(含已注册清单与配置指引)。 */
|
|
92
|
+
export function getEngine(id: string): EnginePort {
|
|
93
|
+
const slot = getRegistrySlot();
|
|
94
|
+
const cached = slot.singletons.get(id);
|
|
95
|
+
if (cached) return cached;
|
|
96
|
+
const factory = slot.factories.get(id);
|
|
97
|
+
if (!factory) {
|
|
98
|
+
throw new EngineNotFoundError(id, listEngines());
|
|
99
|
+
}
|
|
100
|
+
const engine = factory();
|
|
101
|
+
slot.singletons.set(id, engine);
|
|
102
|
+
return engine;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** id 是否已注册(agent 解析期的配置校验入口,D9——不取实例、不触发工厂副作用)。 */
|
|
106
|
+
export function hasEngine(id: string): boolean {
|
|
107
|
+
return getRegistrySlot().factories.has(id);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 已注册引擎 id 清单(稳定序 = 注册序;错误文案与 GUI 引擎选择器共用)。 */
|
|
111
|
+
export function listEngines(): string[] {
|
|
112
|
+
return [...getRegistrySlot().factories.keys()];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 清空注册表(测试隔离专用:防止用例间工厂/单例泄漏串扰)。
|
|
117
|
+
* 生产代码禁用——进程内注册表是全局状态,清空会让已获取的引擎句柄与新注册表脱钩。
|
|
118
|
+
*/
|
|
119
|
+
export function clearEngines(): void {
|
|
120
|
+
const slot = getRegistrySlot();
|
|
121
|
+
slot.factories.clear();
|
|
122
|
+
slot.singletons.clear();
|
|
123
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// src/execution/engine/routing.ts
|
|
2
|
+
//
|
|
3
|
+
// 配置路由与探针 fallback 编排(P4)。设计权威源:
|
|
4
|
+
// docs/architecture/subagent-engine-abstraction.md D9(配置路由三层 + 故障 fallback
|
|
5
|
+
// 三守卫 + model/engine 正交 + workflow 脚本不写死 engine)+ D7(探针分级与触发时机)
|
|
6
|
+
// + §3.3.3 错误规格(engine_probe_failed / model_not_available 行)。
|
|
7
|
+
//
|
|
8
|
+
// 职责边界:本模块只做「选哪个引擎」的决策(纯路由 + probe 编排),不 spawn、不读
|
|
9
|
+
// 任务正文。三层优先级与守卫的判定规则集中于此单一权威点——上层(SAR)只消费
|
|
10
|
+
// routeEngine 的结果,散落的 if engine === ... 分派被结构性排除。
|
|
11
|
+
//
|
|
12
|
+
// probe 触发时机(D7「引擎 factory 初始化与版本变化检测时触发」的 P4 落地口径):
|
|
13
|
+
// 路由期触发、结果缓存于引擎实例(probeCache)。缺省引擎 'pi' 免探——pi 契约稳定
|
|
14
|
+
// (rpc.md 官方,D7 稳定性光谱的轻探针端)且「缺省路径行为零变化」是 A1 硬约束
|
|
15
|
+
// (每次 run 前强探会引入 pi --version 子进程开销与新的失败面);显式 engine='pi'
|
|
16
|
+
// 同样免探(fallback 无处可去,守卫 a 对 pi 不可达是自然结果而非缺口)。进程存活
|
|
17
|
+
// 期间缓存不失效——版本变化(运行中 CLI 被升级)由 engine_run_failed 运行中兜底
|
|
18
|
+
// (D11 规则②封边界的既有设计),重启进程 / registerEngine 覆盖后重探。
|
|
19
|
+
|
|
20
|
+
import { EngineError } from "./common/errors.ts";
|
|
21
|
+
import type { EnginePort } from "./port.ts";
|
|
22
|
+
import { DEFAULT_ENGINE_ID, EngineNotFoundError, getEngine, hasEngine, listEngines } from "./registry.ts";
|
|
23
|
+
import type { ProbeReport } from "./types.ts";
|
|
24
|
+
|
|
25
|
+
// ============================================================
|
|
26
|
+
// 三层优先级(D9)
|
|
27
|
+
// ============================================================
|
|
28
|
+
|
|
29
|
+
/** 三层路由的输入(各层值由调用方装配;undefined = 该层不指定)。 */
|
|
30
|
+
export interface EngineRoutingInput {
|
|
31
|
+
/** 第一层:调用参数 engine(workflow step 级 / AgentCallOpts.engine)。 */
|
|
32
|
+
callEngine?: string;
|
|
33
|
+
/** 第二层:agent .md frontmatter engine(解析期已对注册表校验)。 */
|
|
34
|
+
agentEngine?: string;
|
|
35
|
+
/** 第三层:全局默认引擎(config.json defaultEngine;缺省 'pi')。 */
|
|
36
|
+
globalDefaultEngine?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 生效层标记(守卫 a 的判据:'call' = 显式指定,probe 失败不兜底)。 */
|
|
40
|
+
export type EngineRoutingSource = "call" | "frontmatter" | "default";
|
|
41
|
+
|
|
42
|
+
export interface EngineRouting {
|
|
43
|
+
engineId: string;
|
|
44
|
+
source: EngineRoutingSource;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 非空文本判据:路由层各配置入口统一口径——undefined 与空串同视为未指定。 */
|
|
48
|
+
function hasText(v: string | undefined): v is string {
|
|
49
|
+
return v !== undefined && v !== "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 纯三层解析:调用参数 > agent frontmatter > 全局默认(缺省 'pi')。
|
|
54
|
+
* 不校验注册表(frontmatter 层已前置校验;调用参数层的校验归 routeEngine)——
|
|
55
|
+
* 保持纯函数可独立单测。
|
|
56
|
+
*/
|
|
57
|
+
export function resolveEngineRouting(input: EngineRoutingInput): EngineRouting {
|
|
58
|
+
if (hasText(input.callEngine)) {
|
|
59
|
+
return { engineId: input.callEngine, source: "call" };
|
|
60
|
+
}
|
|
61
|
+
if (hasText(input.agentEngine)) {
|
|
62
|
+
return { engineId: input.agentEngine, source: "frontmatter" };
|
|
63
|
+
}
|
|
64
|
+
if (hasText(input.globalDefaultEngine)) {
|
|
65
|
+
return { engineId: input.globalDefaultEngine, source: "default" };
|
|
66
|
+
}
|
|
67
|
+
return { engineId: DEFAULT_ENGINE_ID, source: "default" };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================
|
|
71
|
+
// routeEngine:probe 编排 + fallback 三守卫(D9①/D7)
|
|
72
|
+
// ============================================================
|
|
73
|
+
|
|
74
|
+
/** routeEngine 的参数(probe/getEngine 注入——测试可 mock,SAR 提供生产实现)。 */
|
|
75
|
+
export interface EngineRouteOptions {
|
|
76
|
+
routing: EngineRoutingInput;
|
|
77
|
+
/**
|
|
78
|
+
* 显式 model(守卫 c 判据:model 与引擎 provider 体系绑定,D9②)。短名 model 的
|
|
79
|
+
* provider 缺省决策在 zcode preparer 的 defaultProviderForShortName——显式默认引擎
|
|
80
|
+
* 模型配置(config.json per-engine model)引入时,两处须同步让位配置值优先
|
|
81
|
+
* (对齐点⑦,详见 preparer.ts 该函数注释)。
|
|
82
|
+
*/
|
|
83
|
+
taskModel?: string;
|
|
84
|
+
/** engineRouting.strict(config.json):true = 一切 probe 失败直接报错。 */
|
|
85
|
+
strict: boolean;
|
|
86
|
+
/** 探针执行体(返回 ProbeReport;引擎实例内部有缓存语义)。 */
|
|
87
|
+
probe: (engineId: string) => Promise<ProbeReport>;
|
|
88
|
+
/** 引擎获取(缺省 registry.getEngine;测试/SAR 可注入)。 */
|
|
89
|
+
getEngineFn?: (engineId: string) => EnginePort;
|
|
90
|
+
/** 注册表存在性检查(缺省 registry.hasEngine)。 */
|
|
91
|
+
hasEngineFn?: (engineId: string) => boolean;
|
|
92
|
+
/** 注册表清单(缺省 registry.listEngines——engine_not_found 文案的数据源)。 */
|
|
93
|
+
listEnginesFn?: () => string[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface EngineRouteResult {
|
|
97
|
+
engine: EnginePort;
|
|
98
|
+
/** 实际执行引擎 id(fallback 后可能 ≠ 请求值)。 */
|
|
99
|
+
engineId: string;
|
|
100
|
+
/** 路由决策时的请求引擎 id(fallback 留痕的 from 值)。 */
|
|
101
|
+
requestedEngineId: string;
|
|
102
|
+
/** 生效层(守卫 a 判据的留痕)。 */
|
|
103
|
+
source: EngineRoutingSource;
|
|
104
|
+
/** fallback 留痕(record/outcome 投影,GUI 警告条数据源)。无 fallback 缺省。 */
|
|
105
|
+
engineFallback?: { from: string; reason: string };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 路由 + 探针 + fallback 编排(SAR run 入口调用)。
|
|
110
|
+
*
|
|
111
|
+
* 失败形态(全部抛结构化错误,调用方转 AgentResult.error):
|
|
112
|
+
* - 未注册 id(调用参数层漏网):EngineNotFoundError(engine_not_found)
|
|
113
|
+
* - strict 或守卫命中:EngineError(engine_probe_failed)
|
|
114
|
+
* - 守卫 c(显式 model + 将换引擎):EngineError(model_not_available)
|
|
115
|
+
*/
|
|
116
|
+
export async function routeEngine(opts: EngineRouteOptions): Promise<EngineRouteResult> {
|
|
117
|
+
const has = opts.hasEngineFn ?? hasEngine;
|
|
118
|
+
const get = opts.getEngineFn ?? getEngine;
|
|
119
|
+
const routing = resolveEngineRouting(opts.routing);
|
|
120
|
+
|
|
121
|
+
// 调用参数层的注册表校验(frontmatter 层已在 agent 解析期抛过;这里兜调用参数
|
|
122
|
+
// 直传的未注册 id——错误含注册清单,配置错误前置到路由期而非 getEngine 深处)
|
|
123
|
+
if (!has(routing.engineId)) {
|
|
124
|
+
throw new EngineNotFoundError(routing.engineId, opts.listEnginesFn?.() ?? listEngines(), describeRoutingSource(opts.routing));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 缺省引擎免探(见文件头「probe 触发时机」)——直接取引擎
|
|
128
|
+
if (routing.engineId === DEFAULT_ENGINE_ID) {
|
|
129
|
+
return {
|
|
130
|
+
engine: get(routing.engineId),
|
|
131
|
+
engineId: routing.engineId,
|
|
132
|
+
requestedEngineId: routing.engineId,
|
|
133
|
+
source: routing.source,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const report = await opts.probe(routing.engineId);
|
|
138
|
+
if (report.ok) {
|
|
139
|
+
return {
|
|
140
|
+
engine: get(routing.engineId),
|
|
141
|
+
engineId: routing.engineId,
|
|
142
|
+
requestedEngineId: routing.engineId,
|
|
143
|
+
source: routing.source,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── probe 失败:strict / 三守卫 / fallback(D9①)──
|
|
148
|
+
if (opts.strict) {
|
|
149
|
+
throw probeFailedError(routing.engineId, report, "engineRouting.strict=true:probe 失败一律报错(不 fallback)");
|
|
150
|
+
}
|
|
151
|
+
// 守卫 a/b(首期合流):显式指定(调用参数或 step 级)= 能力依赖声明,静默换引擎
|
|
152
|
+
// 违反意图——沙箱类任务被静默卸除安全能力正是要防的形态(D9① 原文)。守卫 b 的
|
|
153
|
+
// 独立载体(AgentTaskSpec.requires)下钻后在本分支前独立判定,首期显式 engine 即声明。
|
|
154
|
+
if (routing.source === "call") {
|
|
155
|
+
throw probeFailedError(routing.engineId, report, "engine 来自调用参数显式指定(能力依赖声明)——不兜底");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const fallbackId = fallbackTargetId(opts.routing, routing);
|
|
159
|
+
// 守卫 c:显式 model 与引擎 provider 体系绑定(D9② model/engine 正交)——换引擎
|
|
160
|
+
// 后 model 可解析性无法保证,静默换引擎跑 = 「以为用了 X 实际用 Y」。判定取保守
|
|
161
|
+
// 口径(显式 model + 引擎切换即拒):路由层无各引擎 provider 注册表的访问面,
|
|
162
|
+
// 精确可解析性判定归引擎 prepare 期(ZcodePrepareError.model_not_available 已有)。
|
|
163
|
+
if (hasText(opts.taskModel) && fallbackId !== routing.engineId) {
|
|
164
|
+
throw new EngineError(
|
|
165
|
+
"model_not_available",
|
|
166
|
+
`engine '${routing.engineId}' probe 失败且任务显式指定 model '${opts.taskModel}'——model 与引擎 provider 体系绑定,换引擎(fallback 到 '${fallbackId}')不静默执行`,
|
|
167
|
+
`修复 engine '${routing.engineId}' 的探针失败(见上方恢复指引)后重试,或去掉 model 指定 / 显式传 engine: '${fallbackId}' 确认模型可用后再派发`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
engine: get(fallbackId),
|
|
173
|
+
engineId: fallbackId,
|
|
174
|
+
requestedEngineId: routing.engineId,
|
|
175
|
+
source: "default",
|
|
176
|
+
engineFallback: { from: routing.engineId, reason: "engine_probe_failed" },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* fallback 目标:请求来自 frontmatter/调用参数 → 全局默认引擎;请求即全局默认
|
|
182
|
+
* (defaultEngine 配了坏引擎,source='default')→ 内置缺省 'pi'(零风险回退——设计
|
|
183
|
+
* 终态四口径:fallback 回「缺省 pi」而非原地重试坏引擎)。全局默认与请求引擎相同
|
|
184
|
+
* (frontmatter engine === defaultEngine,如 agent .md engine: zcode + config
|
|
185
|
+
* defaultEngine: zcode)同样回 'pi'——回退到刚 probe 失败的同一引擎 = 原地重试坏
|
|
186
|
+
* 引擎(from==to 误导留痕),违背「回缺省 pi」的设计意图。
|
|
187
|
+
*/
|
|
188
|
+
function fallbackTargetId(routingInput: EngineRoutingInput, resolved: EngineRouting): string {
|
|
189
|
+
// source 与 engineId 由 resolved 承载配对关系,杜绝调用方传错配对的口子
|
|
190
|
+
if (resolved.source === "default") return DEFAULT_ENGINE_ID;
|
|
191
|
+
const global = routingInput.globalDefaultEngine;
|
|
192
|
+
if (hasText(global) && global !== DEFAULT_ENGINE_ID && global !== resolved.engineId) {
|
|
193
|
+
return global;
|
|
194
|
+
}
|
|
195
|
+
return DEFAULT_ENGINE_ID;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** engine_probe_failed 的结构化构造(detail 含逐 check 摘要,recovery 用探针产出)。 */
|
|
199
|
+
function probeFailedError(engineId: string, report: ProbeReport, guard: string): EngineError {
|
|
200
|
+
const checks = report.checks.map((c) => `${c.name}:${c.ok ? "ok" : "FAIL"}`).join(", ");
|
|
201
|
+
return new EngineError(
|
|
202
|
+
"engine_probe_failed",
|
|
203
|
+
`engine '${engineId}' probe 失败(${guard})。checks: [${checks}]`,
|
|
204
|
+
report.error?.recovery ??
|
|
205
|
+
`Confirm the engine binary and version, then re-run the probe (probe({force:true}) or re-initialize the engine).`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 路由来源描述(EngineNotFoundError 的 source 定位)。 */
|
|
210
|
+
function describeRoutingSource(routing: EngineRoutingInput): string | undefined {
|
|
211
|
+
if (hasText(routing.callEngine)) {
|
|
212
|
+
return `call parameter engine='${routing.callEngine}'`;
|
|
213
|
+
}
|
|
214
|
+
if (hasText(routing.agentEngine)) {
|
|
215
|
+
return `agent frontmatter engine='${routing.agentEngine}'`;
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|