@zhushanwen/pi-subagent-workflow 0.1.0 → 0.2.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/agents/context-builder.md +1 -3
- package/agents/oracle.md +2 -2
- package/agents/planner.md +1 -3
- package/agents/researcher.md +0 -2
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +13 -3
- package/agents/worker.md +0 -2
- package/package.json +5 -3
- package/skills/workflow-script-format/SKILL.md +6 -6
- package/src/execution/__tests__/agent-result-mapper.test.ts +24 -2
- package/src/execution/__tests__/concurrency-pool.test.ts +33 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +40 -8
- package/src/execution/__tests__/gui-mode-dispatch.test.ts +60 -0
- package/src/execution/__tests__/sdk-contract.test.ts +5 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +20 -0
- package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +72 -3
- package/src/execution/__tests__/tool-action.test.ts +26 -4
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/concurrency-pool.ts +38 -6
- package/src/execution/execute-options-mapper.ts +21 -4
- package/src/execution/execution-record.ts +5 -0
- package/src/execution/record-store.ts +2 -0
- package/src/execution/session-reconstructor.ts +11 -0
- package/src/execution/session-runner.ts +12 -0
- package/src/execution/stream-sink.ts +83 -0
- package/src/execution/subagent-service.ts +68 -43
- package/src/execution/subprocess-agent-runner.ts +16 -4
- package/src/execution/types.ts +23 -3
- package/src/index.ts +15 -2
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +84 -0
- package/src/interface/__tests__/workflow-state-file-exposure.test.ts +38 -0
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +50 -0
- package/src/interface/command-actions.ts +77 -0
- package/src/interface/commands.ts +40 -4
- package/src/interface/gui-mappers.ts +83 -0
- package/src/interface/helpers.ts +52 -9
- package/src/interface/list-component.ts +3 -1
- package/src/interface/subagent-actions.ts +35 -22
- package/src/interface/subagent-tool.ts +54 -23
- package/src/interface/subagents.ts +45 -5
- package/src/interface/tool-render.ts +16 -5
- package/src/interface/tool-workflow-script.ts +113 -15
- package/src/interface/tool-workflow.ts +92 -34
- package/src/interface/views/WorkflowsView.ts +13 -4
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +70 -0
- package/src/interface/views/detail-content.ts +20 -0
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +208 -0
- package/src/orchestration/__tests__/agent-call-stream.test.ts +157 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +2 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +177 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +15 -0
- package/src/orchestration/agent-opts-resolver.ts +11 -2
- package/src/orchestration/error-recovery.ts +131 -23
- package/src/orchestration/execute-agent-call.ts +12 -3
- package/src/orchestration/jsonl-run-store.ts +10 -0
- package/src/orchestration/lifecycle.ts +1 -1
- package/src/orchestration/models/agent-call.ts +7 -0
- package/src/orchestration/models/ports.ts +15 -2
- package/src/orchestration/models/run-spec.ts +6 -0
- package/src/orchestration/models/trace.ts +1 -0
- package/src/orchestration/models/types.ts +19 -0
- package/src/orchestration/node-ops.ts +2 -0
- package/src/orchestration/worker-script-builder.ts +1 -0
- package/workflows/README.md +58 -0
- package/workflows/chain.js +107 -0
- package/workflows/map-reduce.js +142 -0
- package/workflows/parallel.js +131 -0
- package/workflows/scatter-gather.js +146 -0
- package/examples/README.md +0 -43
- package/examples/chain.example.js +0 -92
- package/examples/map-reduce.example.js +0 -99
- package/examples/parallel.example.js +0 -82
- package/examples/scatter-gather.example.js +0 -106
- package/src/interface/gui-adapter.ts +0 -136
|
@@ -14,6 +14,11 @@ import { createHash } from "node:crypto";
|
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
|
|
17
|
+
import type { AgentResult as WorkflowAgentResult } from "../orchestration/models/types.ts";
|
|
18
|
+
// D-A10: workflow 侧 AgentResult 映射(executeAndAwait 出口)
|
|
19
|
+
import { mapToWorkflowAgentResult } from "./agent-result-mapper.ts";
|
|
20
|
+
import { removeAliveMarker } from "./alive-store.ts";
|
|
21
|
+
import { bestEffort } from "./best-effort.ts";
|
|
17
22
|
import { type ConcurrencyPool,DefaultConcurrencyPool } from "./concurrency-pool.ts";
|
|
18
23
|
import {
|
|
19
24
|
completeRecord,
|
|
@@ -22,10 +27,19 @@ import {
|
|
|
22
27
|
snapshot,
|
|
23
28
|
tryTransition,
|
|
24
29
|
} from "./execution-record.ts";
|
|
30
|
+
import { writeFinalized } from "./finalized-marker.ts";
|
|
31
|
+
import type { ModelConfigService } from "./model-config-service.ts";
|
|
25
32
|
import type { AgentConfig, ModelInfo, ResolvedModel } from "./model-resolver.ts";
|
|
33
|
+
import type { BgNotifyRecord, NotifierHost } from "./notifier.ts";
|
|
34
|
+
import { BgNotifier } from "./notifier.ts";
|
|
26
35
|
import { getSubagentSessionDir } from "./path-encoding.ts";
|
|
36
|
+
import type { StatusFilter } from "./record-store.ts";
|
|
37
|
+
import { RecordStore } from "./record-store.ts";
|
|
27
38
|
import { MAX_FORK_DEPTH } from "./session-context-resolver.ts";
|
|
28
39
|
import { killAllSpawnedChildren, runSpawn, type SessionRunnerContext } from "./session-runner.ts";
|
|
40
|
+
import type { StreamSink } from "./stream-sink.ts";
|
|
41
|
+
import { SubagentStream } from "./stream-sink.ts";
|
|
42
|
+
import { writeCancelledTombstone } from "./tombstone-store.ts";
|
|
29
43
|
import type { WorktreeHandle } from "./types.ts";
|
|
30
44
|
import type {
|
|
31
45
|
AgentEvent,
|
|
@@ -40,20 +54,7 @@ import type {
|
|
|
40
54
|
} from "./types.ts";
|
|
41
55
|
import { ForkDepthExceededError } from "./types.ts";
|
|
42
56
|
import { DEFAULT_AGENT_NAME } from "./types.ts";
|
|
43
|
-
import { bestEffort } from "./best-effort.ts";
|
|
44
|
-
import { removeAliveMarker } from "./alive-store.ts";
|
|
45
|
-
import { writeFinalized } from "./finalized-marker.ts";
|
|
46
|
-
import type { StatusFilter } from "./record-store.ts";
|
|
47
|
-
import { RecordStore } from "./record-store.ts";
|
|
48
|
-
import { writeCancelledTombstone } from "./tombstone-store.ts";
|
|
49
|
-
|
|
50
|
-
// D-A10: workflow 侧 AgentResult 映射(executeAndAwait 出口)
|
|
51
|
-
import { mapToWorkflowAgentResult } from "./agent-result-mapper.ts";
|
|
52
|
-
import type { AgentResult as WorkflowAgentResult } from "../orchestration/models/types.ts";
|
|
53
|
-
import type { ModelConfigService } from "./model-config-service.ts";
|
|
54
57
|
import { WorktreeManager } from "./worktree-manager.ts";
|
|
55
|
-
import { BgNotifier } from "./notifier.ts";
|
|
56
|
-
import type { BgNotifyRecord, NotifierHost } from "./notifier.ts";
|
|
57
58
|
|
|
58
59
|
/** Pi ExtensionAPI 的最小接口(duck-typed)。
|
|
59
60
|
* subagent-service 直接调 pi.sendMessage 发 background 完成通知(BgNotifier 滑动窗口合并),
|
|
@@ -67,6 +68,11 @@ interface PiLike {
|
|
|
67
68
|
): void;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/** UI streaming sink 的最小接口(ctx.ui.setWidget 的 duck-typed 子集)。
|
|
72
|
+
* session_start 时从 ctx.ui 注入,background 执行期间用于把合并后的 text_delta
|
|
73
|
+
* 通过 setWidget 通道转发到 RPC stdout(不经 sendMessage 的持久化路径)。 */
|
|
74
|
+
export type { StreamSink } from "./stream-sink.ts";
|
|
75
|
+
|
|
70
76
|
/** pending-notifications 注册/注销 helper(避免重复代码)。 */
|
|
71
77
|
function emitPendingRegister(pi: PiLike | null, id: string, name?: string): void {
|
|
72
78
|
pi?.events.emit("pending:register", {
|
|
@@ -100,6 +106,8 @@ export interface SubagentServiceInit {
|
|
|
100
106
|
export interface SubagentServiceSessionInit {
|
|
101
107
|
pi: PiLike;
|
|
102
108
|
sessionId: string;
|
|
109
|
+
/** UI streaming sink(ctx.ui.setWidget),用于 background text_delta 转发。 */
|
|
110
|
+
streamSink?: StreamSink;
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
/** background 优先级(保留 priority 排序机制,单一值)。 */
|
|
@@ -162,6 +170,9 @@ export class SubagentService {
|
|
|
162
170
|
private pi: PiLike | null = null;
|
|
163
171
|
/** 当前 Pi session ID(session 隔离过滤用)。initSession 时注入。 */
|
|
164
172
|
private sessionId: string | null = null;
|
|
173
|
+
/** UI streaming sink(ctx.ui.setWidget)。workflow 域经 getStreamSink() 取用。 */
|
|
174
|
+
private streamSink: StreamSink | null = null;
|
|
175
|
+
getStreamSink(): StreamSink | null { return this.streamSink; }
|
|
165
176
|
private _disposed = false;
|
|
166
177
|
private _seq = 0;
|
|
167
178
|
/** background 完成通知器(滑动窗口合并 + 去重)。session_start revive,shutdown dispose。 */
|
|
@@ -198,6 +209,7 @@ export class SubagentService {
|
|
|
198
209
|
initSession(init: SubagentServiceSessionInit): void {
|
|
199
210
|
this.pi = init.pi;
|
|
200
211
|
this.sessionId = init.sessionId;
|
|
212
|
+
this.streamSink = init.streamSink ?? null;
|
|
201
213
|
// [SPAWN fork depth 跨进程传递] 子进程被父 spawn 时,父通过 env
|
|
202
214
|
// PI_SUBAGENT_FORK_DEPTH 传入当前 fork 链深度。子进程 session_start 时
|
|
203
215
|
// 读取作为 forkDepthAls 基线,使后续嵌套 spawn fork 能从正确深度递增。
|
|
@@ -430,6 +442,7 @@ export class SubagentService {
|
|
|
430
442
|
opts: ExecuteOptions,
|
|
431
443
|
signal?: AbortSignal,
|
|
432
444
|
onEvent?: (event: AgentEvent) => void,
|
|
445
|
+
stream?: SubagentStream,
|
|
433
446
|
): Promise<WorkflowAgentResult> {
|
|
434
447
|
this.assertReady();
|
|
435
448
|
|
|
@@ -455,8 +468,7 @@ export class SubagentService {
|
|
|
455
468
|
// ── 步骤 4: signal 决议 ──
|
|
456
469
|
const effectiveSignal = signal ?? record.controller?.signal;
|
|
457
470
|
|
|
458
|
-
//
|
|
459
|
-
// BC-11:onUpdate 置 undefined(不回流 tool UI 细节),onEvent 独立传(AgentEvent 透传 workflow)
|
|
471
|
+
// 步骤 5: runAndFinalize(await,不 detached)。onUpdate=undefined(BC-11),onEvent 独立传,stream 透传。
|
|
460
472
|
const result = await this.runAndFinalize(
|
|
461
473
|
record,
|
|
462
474
|
{ ...opts, onUpdate: undefined },
|
|
@@ -465,6 +477,7 @@ export class SubagentService {
|
|
|
465
477
|
effectiveSignal,
|
|
466
478
|
PRIORITY_BACKGROUND,
|
|
467
479
|
onEvent,
|
|
480
|
+
stream,
|
|
468
481
|
);
|
|
469
482
|
|
|
470
483
|
// ── 步骤 6: D-A10 AgentResult 映射 ──
|
|
@@ -538,6 +551,7 @@ export class SubagentService {
|
|
|
538
551
|
thinkingLevel: identity.resolved.thinkingLevel,
|
|
539
552
|
mode,
|
|
540
553
|
task: opts.task,
|
|
554
|
+
slug: opts.slug,
|
|
541
555
|
startedAt: Date.now(),
|
|
542
556
|
rootSessionId: this.sessionId ?? undefined,
|
|
543
557
|
parentRecordId,
|
|
@@ -567,13 +581,20 @@ export class SubagentService {
|
|
|
567
581
|
signal: AbortSignal | undefined,
|
|
568
582
|
priority: number,
|
|
569
583
|
rawOnEvent?: (event: AgentEvent) => void,
|
|
584
|
+
stream?: SubagentStream,
|
|
570
585
|
): Promise<AgentResult> {
|
|
571
|
-
// 仅 background 进并发池限流,分层配额:每层嵌套 depth 让有效配额 -1(下限 1)。
|
|
572
|
-
// 顶层 depth=0 拿满配额;嵌套越深有效并发越小,防子 agent fan-out 压垮主 agent 的 pool。
|
|
573
586
|
const pooled = record.mode === "background";
|
|
587
|
+
let acquired = false;
|
|
574
588
|
if (pooled) {
|
|
575
589
|
const effectiveMaxConcurrent = Math.max(1, this.pool.maxConcurrent - record.depth);
|
|
576
|
-
|
|
590
|
+
try {
|
|
591
|
+
await this.pool.acquire(priority, effectiveMaxConcurrent, signal);
|
|
592
|
+
acquired = true;
|
|
593
|
+
} catch {
|
|
594
|
+
// S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致。
|
|
595
|
+
if (signal?.aborted) return this.finalizeAborted(record);
|
|
596
|
+
return this.finalizeFailed(record, new Error("aborted"));
|
|
597
|
+
}
|
|
577
598
|
}
|
|
578
599
|
// onEvent 包装:AgentEvent → onUpdate(project(record)) 回流调用方
|
|
579
600
|
const onEvent = rawOnEvent
|
|
@@ -581,16 +602,12 @@ export class SubagentService {
|
|
|
581
602
|
? (event: AgentEvent): void => this.onEventThrottled(record, event, opts.onUpdate!)
|
|
582
603
|
: undefined);
|
|
583
604
|
|
|
584
|
-
// 解析 worktree 参数:boolean → WorktreeHandle | undefined
|
|
605
|
+
// 解析 worktree 参数:boolean → WorktreeHandle | undefined(true/undefined 由 run 内部处理)
|
|
585
606
|
let worktreeHandle: WorktreeHandle | undefined;
|
|
586
607
|
if (typeof opts.worktree === "object") {
|
|
587
608
|
worktreeHandle = opts.worktree;
|
|
588
609
|
}
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
// [MF#4][MF#2] fork 深度护栏:深度按 async 调用链传递(ALS),不再用共享实例计数器。
|
|
592
|
-
// parentDepth = 当前调用链的深度(主 session 链上无 store→0);fork 时推进为 parentDepth+1,
|
|
593
|
-
// 包进 run() 的 ALS 作用域,使子 agent 在 prompt() 期间发起的嵌套 execute 能读到该深度。
|
|
610
|
+
// [MF#4][MF#2] fork 深度护栏:ALS 传递深度(主 session 链无 store→0,fork 推进 +1)。
|
|
594
611
|
const parentDepth = this.forkDepthAls.getStore() ?? 0;
|
|
595
612
|
const effectiveDepth = opts.fork ? parentDepth + 1 : parentDepth;
|
|
596
613
|
|
|
@@ -612,6 +629,7 @@ export class SubagentService {
|
|
|
612
629
|
graceTurns: opts.graceTurns,
|
|
613
630
|
signal,
|
|
614
631
|
onEvent,
|
|
632
|
+
stream, // text_delta streaming(background 路径有值,workflow 路径 undefined)
|
|
615
633
|
fork: opts.fork,
|
|
616
634
|
worktree: worktreeHandle,
|
|
617
635
|
parentForkDepth: parentDepth, // [MF#4] 父链深度,不从 opts 读
|
|
@@ -626,7 +644,9 @@ export class SubagentService {
|
|
|
626
644
|
result = await this.finalizeFailed(record, err);
|
|
627
645
|
return result;
|
|
628
646
|
} finally {
|
|
629
|
-
if (pooled) this.pool.release();
|
|
647
|
+
if (pooled && acquired) this.pool.release();
|
|
648
|
+
// 清除 streaming widget(subagent 终态,幂等)
|
|
649
|
+
stream?.dispose();
|
|
630
650
|
}
|
|
631
651
|
|
|
632
652
|
// status 唯一判定点:success ? done : (aborted ? cancelled : failed)
|
|
@@ -650,7 +670,15 @@ export class SubagentService {
|
|
|
650
670
|
signal: AbortSignal | undefined,
|
|
651
671
|
priority: number,
|
|
652
672
|
): void {
|
|
653
|
-
|
|
673
|
+
// 创建 streaming 生命周期对象——streamSink 为 null(session_start 未注入)时降级为 undefined。
|
|
674
|
+
const stream = this.streamSink
|
|
675
|
+
? new SubagentStream(record.id, this.streamSink)
|
|
676
|
+
: undefined;
|
|
677
|
+
|
|
678
|
+
void this.runAndFinalize(
|
|
679
|
+
record, opts, ctx, identity, signal, priority,
|
|
680
|
+
undefined, stream,
|
|
681
|
+
)
|
|
654
682
|
.then(() => {
|
|
655
683
|
// background 回注:仅当本路径抢到 CAS(status 已转 done/failed)才 notify。
|
|
656
684
|
// cancel 抢先时 status=cancelled,cancelBackground 自己 notify,此处跳过。
|
|
@@ -732,10 +760,9 @@ export class SubagentService {
|
|
|
732
760
|
): Promise<void> {
|
|
733
761
|
// 终态清节流状态:防 trailing timer 在 record 归档后误发陈旧 onUpdate
|
|
734
762
|
this.clearThrottle(record.id);
|
|
735
|
-
// ── Step 0: collectPatch(best-effort
|
|
763
|
+
// ── Step 0: collectPatch(best-effort)──
|
|
736
764
|
// [MF#3] patchFile 写到 worktree 之外(sessionsDir/<branch>.patch),避免被 cleanup 删除;
|
|
737
765
|
// 路径回填 record.patchFile,供调用方(tool result / /subagents list)应用。
|
|
738
|
-
let patchOk = true;
|
|
739
766
|
if (record.worktreeHandle) {
|
|
740
767
|
try {
|
|
741
768
|
const sessionsDir = getSubagentSessionDir(
|
|
@@ -745,12 +772,9 @@ export class SubagentService {
|
|
|
745
772
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
746
773
|
const patchFile = path.join(sessionsDir, `${record.worktreeHandle.branch}.patch`);
|
|
747
774
|
const patch = this.worktreeManager.collectPatch(record.worktreeHandle, patchFile);
|
|
748
|
-
patchOk = !patch.failed;
|
|
749
|
-
// 仅 patch 实际写盘(非空 diff 且未失败)才回填,避免指向不存在文件的悬空路径——
|
|
750
|
-
// 否则 notifier/render/sync 路径会向 LLM 输出 `git apply <不存在>`(纯查询任务命中)。
|
|
751
775
|
if (patch.written) record.patchFile = patchFile;
|
|
752
|
-
} catch {
|
|
753
|
-
|
|
776
|
+
} catch (pe: unknown) {
|
|
777
|
+
bestEffort(pe, "collectPatch (finalizeRecord Step0)");
|
|
754
778
|
}
|
|
755
779
|
}
|
|
756
780
|
|
|
@@ -787,7 +811,7 @@ export class SubagentService {
|
|
|
787
811
|
bestEffort(err, "writeFinalized/tombstone (finalizeRecord Step3)");
|
|
788
812
|
}
|
|
789
813
|
}
|
|
790
|
-
if (record.worktreeHandle
|
|
814
|
+
if (record.worktreeHandle) {
|
|
791
815
|
try {
|
|
792
816
|
this.worktreeManager.cleanup(record.worktreeHandle);
|
|
793
817
|
} catch (err) {
|
|
@@ -816,15 +840,7 @@ export class SubagentService {
|
|
|
816
840
|
private async finalizeFailed(record: ExecutionRecord, err: unknown): Promise<AgentResult> {
|
|
817
841
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
818
842
|
// durationMs 用真实耗时(startedAt → now),避免失败统计恒为 0 失真。
|
|
819
|
-
const failedResult: AgentResult = {
|
|
820
|
-
text: "",
|
|
821
|
-
turns: record.turnCount,
|
|
822
|
-
durationMs: Date.now() - record.startedAt,
|
|
823
|
-
success: false,
|
|
824
|
-
error: errMsg,
|
|
825
|
-
sessionId: record.id,
|
|
826
|
-
toolCalls: [],
|
|
827
|
-
};
|
|
843
|
+
const failedResult: AgentResult = { text: "", turns: record.turnCount, durationMs: Date.now() - record.startedAt, success: false, error: errMsg, sessionId: record.id, toolCalls: [] };
|
|
828
844
|
// CAS 抢锁:抢到(status 仍 running)则完整收尾;没抢到(cancel 已先设 cancelled)跳过。
|
|
829
845
|
if (tryTransition(record, "failed")) {
|
|
830
846
|
await this.finalizeRecord(record, failedResult, "failed");
|
|
@@ -832,6 +848,15 @@ export class SubagentService {
|
|
|
832
848
|
return failedResult;
|
|
833
849
|
}
|
|
834
850
|
|
|
851
|
+
/** S1: 排队中被 abort 走 cancelled 终态(对齐已运行被 abort 的 cancelBackground)。 */
|
|
852
|
+
private async finalizeAborted(record: ExecutionRecord): Promise<AgentResult> {
|
|
853
|
+
const cancelledResult: AgentResult = { text: "", turns: record.turnCount, durationMs: Date.now() - record.startedAt, success: false, error: "cancelled by user", sessionId: record.id, toolCalls: [] };
|
|
854
|
+
if (tryTransition(record, "cancelled")) {
|
|
855
|
+
await this.finalizeRecord(record, cancelledResult, "cancelled");
|
|
856
|
+
}
|
|
857
|
+
return cancelledResult;
|
|
858
|
+
}
|
|
859
|
+
|
|
835
860
|
// onUpdate 节流状态(per-record Map)。每条 record(每条 onUpdate 回流链)独立节流,
|
|
836
861
|
// 避免嵌套(fork 链:主→A→B)多条 onUpdate 链争用同一份节流状态。
|
|
837
862
|
// [HISTORICAL] 旧实现用单个实例字段,注释假设“fork 嵌套串行,同时只有一条链”——错误:
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
// D-A2(映射归 adapter)/ D-A8(onEvent 桥接)/ D-A9(timeoutMs 合并 signal)/
|
|
15
15
|
// D-008(model 填底,不调 resolveModel)/ BC-9(timeoutMs 行为)/ BC-10(live-record 进度)
|
|
16
16
|
|
|
17
|
-
import type { AgentEvent } from "../shared/agent-event.ts";
|
|
18
17
|
import type { AgentRunner } from "../orchestration/models/ports.ts";
|
|
19
18
|
import type { AgentCallOpts, AgentResult } from "../orchestration/models/types.ts";
|
|
19
|
+
import type { AgentEvent } from "../shared/agent-event.ts";
|
|
20
|
+
import { mapToExecuteOptions, mergeTimeoutSignal } from "./execute-options-mapper.ts";
|
|
20
21
|
import type { ModelInfo } from "./model-resolver.ts";
|
|
22
|
+
import type { SubagentStream } from "./stream-sink.ts";
|
|
21
23
|
import type { SubagentService } from "./subagent-service.ts";
|
|
22
24
|
import type { ExecuteOptions } from "./types.ts";
|
|
23
|
-
import { mapToExecuteOptions, mergeTimeoutSignal } from "./execute-options-mapper.ts";
|
|
24
25
|
|
|
25
26
|
// ── 构造依赖(per-session 注入)──
|
|
26
27
|
|
|
@@ -52,13 +53,23 @@ export interface SubprocessAgentRunnerDeps {
|
|
|
52
53
|
*/
|
|
53
54
|
export class SubprocessAgentRunner implements AgentRunner {
|
|
54
55
|
private readonly subagentService: SubagentService;
|
|
55
|
-
private
|
|
56
|
+
private ctxModel: ModelInfo | undefined;
|
|
56
57
|
|
|
57
58
|
constructor(deps: SubprocessAgentRunnerDeps) {
|
|
58
59
|
this.subagentService = deps.subagentService;
|
|
59
60
|
this.ctxModel = deps.ctxModel;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* 刷新主 agent model 缓存。model_select 事件时由 index.ts 调用。
|
|
65
|
+
* H1 修复:旧实现 ctxModel 是 readonly,session_start 后固化,
|
|
66
|
+
* model_select 只刷新 ModelConfigService._ctxModel 不更新 SAR →
|
|
67
|
+
* workflow 路径用过期模型。
|
|
68
|
+
*/
|
|
69
|
+
updateCtxModel(model: ModelInfo | undefined): void {
|
|
70
|
+
this.ctxModel = model;
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
/**
|
|
63
74
|
* 执行单次 agent 调用:委托 SubagentService.executeAndAwait。
|
|
64
75
|
*
|
|
@@ -75,6 +86,7 @@ export class SubprocessAgentRunner implements AgentRunner {
|
|
|
75
86
|
opts: AgentCallOpts,
|
|
76
87
|
signal: AbortSignal,
|
|
77
88
|
onEvent?: (event: AgentEvent) => void,
|
|
89
|
+
stream?: SubagentStream,
|
|
78
90
|
): Promise<AgentResult> {
|
|
79
91
|
const startedAt = Date.now();
|
|
80
92
|
|
|
@@ -93,7 +105,7 @@ export class SubprocessAgentRunner implements AgentRunner {
|
|
|
93
105
|
const bridgedOnEvent = onEvent;
|
|
94
106
|
|
|
95
107
|
// ── 核心委托 ──
|
|
96
|
-
return await this.subagentService.executeAndAwait(mappedOpts, mergedSignal, bridgedOnEvent);
|
|
108
|
+
return await this.subagentService.executeAndAwait(mappedOpts, mergedSignal, bridgedOnEvent, stream);
|
|
97
109
|
} catch (err) {
|
|
98
110
|
// executeAndAwait throw(嵌套超限 ForkDepthExceededError,BC-12)或未预期异常 → 不 reject,入 error。
|
|
99
111
|
const message = err instanceof Error ? err.message : String(err);
|
package/src/execution/types.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// - Runtime 编排 Core,产出 Details/Record 给 TUI
|
|
8
8
|
// - TUI 只读 Record/Details 快照,永不持有可变引用
|
|
9
9
|
|
|
10
|
+
import type { GuiRenderResult } from "@xyz-agent/extension-protocol";
|
|
11
|
+
|
|
10
12
|
import type { ModelInfo, ModelRegistryLike } from "./model-resolver.ts";
|
|
11
13
|
|
|
12
14
|
// ============================================================
|
|
@@ -304,6 +306,11 @@ export interface ExecutionRecord {
|
|
|
304
306
|
readonly thinkingLevel: string | undefined;
|
|
305
307
|
readonly mode: ExecutionMode;
|
|
306
308
|
readonly task: string;
|
|
309
|
+
/**
|
|
310
|
+
* 人类可读的短标签(≤20 字符),简述本次 subagent「在做什么」。
|
|
311
|
+
* 区别于 agent(类型名)/ task(完整 prompt)。旧持久化 record 反序列化时缺失兜底空串。
|
|
312
|
+
*/
|
|
313
|
+
readonly slug: string;
|
|
307
314
|
readonly startedAt: number;
|
|
308
315
|
/** 根 Pi session ID(session 隔离过滤用)。递归链上所有层 record 同值。 */
|
|
309
316
|
readonly rootSessionId: string | undefined;
|
|
@@ -360,6 +367,8 @@ export interface SubagentToolDetails {
|
|
|
360
367
|
agent: string;
|
|
361
368
|
model: string;
|
|
362
369
|
thinkingLevel: string | undefined;
|
|
370
|
+
/** 短标签(≤20 字符),来自 record.slug。旧 record 反序列化时为空串。 */
|
|
371
|
+
slug: string;
|
|
363
372
|
turns: number;
|
|
364
373
|
totalTokens: number;
|
|
365
374
|
elapsedSeconds: number;
|
|
@@ -385,6 +394,11 @@ export interface SubagentToolDetails {
|
|
|
385
394
|
/** Hub.execute 的入参(sync/bg 共用)。mode 由 Hub 内部判定,不暴露给调用方。 */
|
|
386
395
|
export interface ExecuteOptions {
|
|
387
396
|
task: string;
|
|
397
|
+
/**
|
|
398
|
+
* 短标签(≤20 字符),简述本次执行用途,展示在 TUI。必填。
|
|
399
|
+
* workflow 内 agent() 调用时从 AgentCallOpts.description 透传而来。
|
|
400
|
+
*/
|
|
401
|
+
slug: string;
|
|
388
402
|
agent?: string;
|
|
389
403
|
model?: string;
|
|
390
404
|
thinkingLevel?: string;
|
|
@@ -434,6 +448,8 @@ export type ExecutionHandle = {
|
|
|
434
448
|
export interface SubagentListItem {
|
|
435
449
|
subagentId: string;
|
|
436
450
|
agent: string;
|
|
451
|
+
/** 短标签(≤20 字符),来自 record.slug。旧 record 反序列化时为空串。 */
|
|
452
|
+
slug: string;
|
|
437
453
|
status: ExecutionStatus;
|
|
438
454
|
mode: ExecutionMode;
|
|
439
455
|
/** 运行秒数(running 态实时计算,终态 endedAt-startedAt)。 */
|
|
@@ -473,9 +489,9 @@ export interface CancelResponse {
|
|
|
473
489
|
* - cancel → cancelResponse(subagentId 有值;sessionFile 无意义,可为 null)
|
|
474
490
|
*/
|
|
475
491
|
export type SubagentToolResult =
|
|
476
|
-
| { action: "start"; subagentId: string; sessionFile: string | null; bgResponse: BgResponse }
|
|
477
|
-
| { action: "list"; subagentId: null; sessionFile: null; listResponse: ListResponse }
|
|
478
|
-
| { action: "cancel"; subagentId: string; sessionFile: null; cancelResponse: CancelResponse };
|
|
492
|
+
| { action: "start"; subagentId: string; sessionFile: string | null; slug: string; bgResponse: BgResponse; __gui__?: GuiRenderResult }
|
|
493
|
+
| { action: "list"; subagentId: null; sessionFile: null; listResponse: ListResponse; __gui__?: GuiRenderResult }
|
|
494
|
+
| { action: "cancel"; subagentId: string; sessionFile: null; cancelResponse: CancelResponse; __gui__?: GuiRenderResult };
|
|
479
495
|
|
|
480
496
|
// ============================================================
|
|
481
497
|
// TUI list 视图的合并 record(4 源 merge 后的形状)
|
|
@@ -487,6 +503,8 @@ export interface SubagentRecord {
|
|
|
487
503
|
agent: string;
|
|
488
504
|
/** 任务提示词(详情面板置顶展示)。磁盘/内存源均有。 */
|
|
489
505
|
task: string;
|
|
506
|
+
/** 短标签(≤20 字符)。磁盘重建源旧文件可能缺失→兜底空串。 */
|
|
507
|
+
slug: string;
|
|
490
508
|
status: ExecutionStatus;
|
|
491
509
|
mode: ExecutionMode;
|
|
492
510
|
startedAt: number;
|
|
@@ -552,6 +570,8 @@ export interface RecordSnapshot {
|
|
|
552
570
|
readonly thinkingLevel: string | undefined;
|
|
553
571
|
readonly mode: ExecutionMode;
|
|
554
572
|
readonly task: string;
|
|
573
|
+
/** 短标签(≤20 字符)。来自 record.slug。 */
|
|
574
|
+
readonly slug: string;
|
|
555
575
|
readonly status: ExecutionStatus;
|
|
556
576
|
readonly turns: number;
|
|
557
577
|
readonly totalTokens: number;
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { SubprocessAgentRunner } from "./execution/subprocess-agent-runner.ts";
|
|
|
38
38
|
import { WorktreeManager } from "./execution/worktree-manager.ts";
|
|
39
39
|
import { renderBgNotifyMessage } from "./interface/bg-notify-render.ts";
|
|
40
40
|
import { registerWorkflowsCommand } from "./interface/commands.ts";
|
|
41
|
+
import { toGuiCtx } from "./interface/gui-mappers.ts";
|
|
41
42
|
import { notifyDone } from "./interface/helpers.ts";
|
|
42
43
|
import { registerSubagentTool } from "./interface/subagent-tool.ts";
|
|
43
44
|
// ═══ interface/ 层(tools/commands/tui 合并) ═══
|
|
@@ -168,7 +169,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
168
169
|
runner: state.runner,
|
|
169
170
|
runs: state.runs,
|
|
170
171
|
registry,
|
|
171
|
-
onRunDone: (run: WorkflowRun) => notifyDone(pi, run.runId, run, notifiedRunIds, sessionCtx),
|
|
172
|
+
onRunDone: (run: WorkflowRun) => notifyDone(pi, run.runId, run, notifiedRunIds, toGuiCtx(sessionCtx)),
|
|
172
173
|
agentRegistry: state.agentRegistry,
|
|
173
174
|
sessionDir: state.sessionDir,
|
|
174
175
|
activeTempFiles: state.activeTempFiles,
|
|
@@ -177,6 +178,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
177
178
|
scheduleTimeBudget(runId, deps, budgetTimeMs),
|
|
178
179
|
onWorkflowCall: (name: string, args: Record<string, unknown>, parentRun: WorkflowRun) =>
|
|
179
180
|
executeNestedWorkflow(name, args, parentRun, deps),
|
|
181
|
+
streamSink: getSubagentService()?.getStreamSink() ?? undefined,
|
|
180
182
|
log,
|
|
181
183
|
};
|
|
182
184
|
return deps;
|
|
@@ -214,6 +216,11 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
214
216
|
service.initSession({
|
|
215
217
|
pi,
|
|
216
218
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
219
|
+
// 注入 ctx.ui.setWidget 作为 streaming sink(只绑方法,不持有整个 ctx)。
|
|
220
|
+
// background subagent 执行期间,text_delta 经 SubagentStream 合并后由此通道转发。
|
|
221
|
+
streamSink: {
|
|
222
|
+
setWidget: (key, lines) => ctx.ui.setWidget(key, lines),
|
|
223
|
+
},
|
|
217
224
|
});
|
|
218
225
|
|
|
219
226
|
if (!existingService) {
|
|
@@ -296,11 +303,17 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
296
303
|
// ════════════════════════════════════════════════════════════
|
|
297
304
|
// model_select:用户切换 model 时刷新缓存
|
|
298
305
|
// ════════════════════════════════════════════════════════════
|
|
299
|
-
pi.on("model_select", (event: { model: NonNullable<ExtensionContext["model"]> },
|
|
306
|
+
pi.on("model_select", (event: { model: NonNullable<ExtensionContext["model"]> }, ctx: ExtensionContext) => {
|
|
300
307
|
const service = getModelConfigService();
|
|
301
308
|
if (service && typeof service.setCtxModel === "function") {
|
|
302
309
|
service.setCtxModel(event.model);
|
|
303
310
|
}
|
|
311
|
+
// H1: 同步刷新所有 session 的 SAR ctxModel(旧实现只在 session_start 固化)
|
|
312
|
+
const sid = ctx.sessionManager.getSessionId();
|
|
313
|
+
const state = sessionState.get(sid);
|
|
314
|
+
if (state) {
|
|
315
|
+
state.runner.updateCtxModel(event.model);
|
|
316
|
+
}
|
|
304
317
|
});
|
|
305
318
|
|
|
306
319
|
// ════════════════════════════════════════════════════════════
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// 提示词质量回归:subagent tool description 必须是"行为约束器"而非"功能说明书"。
|
|
2
|
+
//
|
|
3
|
+
// agent(LLM)决策时唯一能看到的 tool 元信息就是 description。审查发现旧版
|
|
4
|
+
// description 把篇幅花在功能说明上,缺乏调用信号(何时委派 vs 自己做)、
|
|
5
|
+
// 能力边界(cannot)、反模式密度(高风险要求 ≥4 条)、以及对 auto-injected
|
|
6
|
+
// completion message 的注入防御。
|
|
7
|
+
//
|
|
8
|
+
// 本测试用源码断言(读 .ts 文件文本)锁定这些约束,防止后续重构把约束措辞
|
|
9
|
+
// 删掉或弱化。读源码而非 import,避免 mock 链(subagent-tool.ts 依赖 pi-ai/
|
|
10
|
+
// typebox/pi-tui/ExtensionAPI 等值导入)。
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { join, dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const SUBAGENT_TOOL_SRC = readFileSync(
|
|
19
|
+
join(__dirname, "../subagent-tool.ts"),
|
|
20
|
+
"utf-8",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/** 提取 description: `...` 模板字符串的原始内容。 */
|
|
24
|
+
function extractDescription(src: string): string {
|
|
25
|
+
const m = src.match(/description:\s*`([\s\S]*?)`,/);
|
|
26
|
+
if (!m) throw new Error("description template literal not found");
|
|
27
|
+
return m[1];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DESCRIPTION = extractDescription(SUBAGENT_TOOL_SRC);
|
|
31
|
+
|
|
32
|
+
describe("subagent tool description — 行为约束器(非功能说明书)", () => {
|
|
33
|
+
it("词数 ≤ 400(高风险 description 密度上限)", () => {
|
|
34
|
+
// 高风险 tool 的 description 应聚焦约束而非功能铺陈;过长会稀释信号。
|
|
35
|
+
const words = DESCRIPTION.trim().split(/\s+/).filter(Boolean).length;
|
|
36
|
+
expect(words).toBeLessThanOrEqual(400);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("含 'When to delegate' 调用条件段(何时委派 vs 自己做)", () => {
|
|
40
|
+
// 开篇必须给信号驱动的调用条件,而非纯功能说明。
|
|
41
|
+
expect(DESCRIPTION).toMatch(/When to delegate/i);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("Anti-patterns 段含 ≥ 4 条 bullet", () => {
|
|
45
|
+
// 高风险 tool 要求 ≥ 4 条反模式,密度才足以覆盖主要误用路径。
|
|
46
|
+
const apIdx = DESCRIPTION.indexOf("## Anti-patterns");
|
|
47
|
+
expect(apIdx).toBeGreaterThan(-1);
|
|
48
|
+
const afterAp = DESCRIPTION.slice(apIdx);
|
|
49
|
+
// 截到下一个 ## 段
|
|
50
|
+
const nextSection = afterAp.indexOf("##", "## Anti-patterns".length);
|
|
51
|
+
const apSection =
|
|
52
|
+
nextSection > -1 ? afterAp.slice(0, nextSection) : afterAp;
|
|
53
|
+
const bullets = apSection.match(/^- .+/gm) || [];
|
|
54
|
+
expect(bullets.length).toBeGreaterThanOrEqual(4);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("含能力边界段 'You cannot'", () => {
|
|
58
|
+
// 必须显式声明 tool 做不到的事,阻止 LLM 错误假设。
|
|
59
|
+
expect(DESCRIPTION).toMatch(/You cannot/);
|
|
60
|
+
expect(DESCRIPTION.toLowerCase()).toContain("cannot");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("含注入防御:声明 completion message 为不可信数据", () => {
|
|
64
|
+
// completion message 是 auto-injected(F14 注入面),必须告诉 LLM
|
|
65
|
+
// 把它当作不可信数据,校验其中的指令后再执行。
|
|
66
|
+
const lower = DESCRIPTION.toLowerCase();
|
|
67
|
+
expect(
|
|
68
|
+
lower.includes("untrusted") || lower.includes("verify"),
|
|
69
|
+
).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("保留 nested spawning 段(允许 sub-subagent,仅深度限制)", () => {
|
|
73
|
+
// 这段防止 LLM 错误拒绝合法的 nested delegation。
|
|
74
|
+
expect(DESCRIPTION).toMatch(/Nested spawning/);
|
|
75
|
+
expect(DESCRIPTION).toMatch(/Depth: N\/10/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("保留 executionMode sequential 的 CRITICAL 说明", () => {
|
|
79
|
+
// sequential 是关键执行语义,删了会导致 LLM 误以为并行可用。
|
|
80
|
+
expect(DESCRIPTION).toMatch(/CRITICAL/i);
|
|
81
|
+
expect(DESCRIPTION).toMatch(/sequential/);
|
|
82
|
+
expect(DESCRIPTION).toMatch(/SAME message/i);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/interface/__tests__/workflow-state-file-exposure.test.ts
|
|
2
|
+
//
|
|
3
|
+
// W2: WorkflowToolDetails 携带 stateFile + toRunSummary 填充逻辑
|
|
4
|
+
//
|
|
5
|
+
// 防的 bug:workflow run 自身的状态文件路径(<sessionDir>/workflow-state/<runId>.jsonl)
|
|
6
|
+
// 从未暴露到 tool details——overlay/GUI 拿不到,无法定位 run 状态文件做后续查看。
|
|
7
|
+
// 本测试验证 toRunSummary 和 actionRun 的 details 包含 stateFile 字段。
|
|
8
|
+
//
|
|
9
|
+
// 使用源码断言(读 .ts 文件文本)避免 import 重 mock 链——与 workflow-tool-prompt.test.ts 同模式。
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { join, dirname } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const TOOL_WORKFLOW_SRC = readFileSync(
|
|
18
|
+
join(__dirname, "../tool-workflow.ts"),
|
|
19
|
+
"utf-8",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
describe("W2: workflow tool details 暴露 stateFile", () => {
|
|
23
|
+
it("RunSummary 类型声明含 stateFile 可选字段", () => {
|
|
24
|
+
// RunSummary 必须声明 stateFile,否则 toRunSummary 返回的对象类型不含此字段
|
|
25
|
+
expect(TOOL_WORKFLOW_SRC).toMatch(/stateFile\?\s*:\s*string/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("toRunSummary 调用 deps.store.stateFilePath 填充 stateFile", () => {
|
|
29
|
+
// toRunSummary 必须从 store.stateFilePath(runId) 取值——这是数据来源
|
|
30
|
+
expect(TOOL_WORKFLOW_SRC).toMatch(/stateFilePath/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("run action 的 details 含 stateFile 字段", () => {
|
|
34
|
+
// actionRun 返回的 details 必须携带 stateFile(run 启动时即暴露路径)
|
|
35
|
+
// 验证 actionRun 函数体中构造 details 时引用了 stateFilePath
|
|
36
|
+
expect(TOOL_WORKFLOW_SRC).toMatch(/action.*run[\s\S]*stateFilePath|stateFilePath[\s\S]*action.*run/i);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// U1: workflow tool 提示词包含内置 workflow 清单 + 交叉引用。
|
|
2
|
+
//
|
|
3
|
+
// agent(LLM)决策时唯一能看到的 tool 元信息就是 description + promptGuidelines。
|
|
4
|
+
// 若这些文本里不提及内置 workflow(chain/parallel/scatter-gather/map-reduce),
|
|
5
|
+
// LLM 无法知道有现成的编排工具可用,会倾向自己 generate 脚本或瞎猜 name。
|
|
6
|
+
//
|
|
7
|
+
// 本测试用源码断言(读 .ts 文件文本)验证提示词内容,避免 import 重 mock 链
|
|
8
|
+
// (tool-workflow.ts 依赖 pi-ai/typebox/pi-tui/lifecycle 等值导入)。
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join, dirname } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const TOOL_WORKFLOW_SRC = readFileSync(
|
|
17
|
+
join(__dirname, "../tool-workflow.ts"),
|
|
18
|
+
"utf-8",
|
|
19
|
+
);
|
|
20
|
+
const TOOL_WORKFLOW_SCRIPT_SRC = readFileSync(
|
|
21
|
+
join(__dirname, "../tool-workflow-script.ts"),
|
|
22
|
+
"utf-8",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
describe("U1: workflow tool prompt mentions built-in workflows", () => {
|
|
26
|
+
it("tool-workflow.ts description 或 promptGuidelines 含 4 个内置 workflow 名称", () => {
|
|
27
|
+
// 4 个内置通用编排 workflow 必须在提示词里出现,LLM 才能发现并使用。
|
|
28
|
+
expect(TOOL_WORKFLOW_SRC).toContain("chain");
|
|
29
|
+
expect(TOOL_WORKFLOW_SRC).toContain("parallel");
|
|
30
|
+
expect(TOOL_WORKFLOW_SRC).toContain("scatter-gather");
|
|
31
|
+
expect(TOOL_WORKFLOW_SRC).toContain("map-reduce");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("tool-workflow.ts promptGuidelines 含 workflow-script list 交叉引用", () => {
|
|
35
|
+
// LLM 需要知道"先 list 再 run"的发现路径——两个 tool 之间必须有交叉引用。
|
|
36
|
+
// 文本可能跨字符串拼接行,分别断言两个关键词都存在。
|
|
37
|
+
expect(TOOL_WORKFLOW_SRC).toContain("workflow-script");
|
|
38
|
+
expect(TOOL_WORKFLOW_SRC).toMatch(/action.*list/i);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("tool-workflow.ts promptGuidelines 含 run action 的正例", () => {
|
|
42
|
+
// 给出 workflow run <name> 的调用示例,LLM 才知道参数格式。
|
|
43
|
+
expect(TOOL_WORKFLOW_SRC).toMatch(/workflow run .+--args/i);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("tool-workflow-script.ts list action 的 promptGuidelines 含 workflow run 交叉引用", () => {
|
|
47
|
+
// 反向交叉引用:list 的指引里要提到用 workflow tool 的 run action 启动脚本。
|
|
48
|
+
expect(TOOL_WORKFLOW_SCRIPT_SRC).toMatch(/workflow.*tool.*run|run.*workflow.*tool/i);
|
|
49
|
+
});
|
|
50
|
+
});
|