@meiyukichan/dsh-sandore 0.1.0-rc.6
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 +201 -0
- package/cordis.patch.yml +39 -0
- package/dsh.plugin.json +16 -0
- package/lib/client/CommandEditor.d.ts +19 -0
- package/lib/client/DagEditor.d.ts +22 -0
- package/lib/client/DefinitionDetail.d.ts +13 -0
- package/lib/client/DefinitionList.d.ts +19 -0
- package/lib/client/DiscussionInput.d.ts +63 -0
- package/lib/client/DragHandle.d.ts +16 -0
- package/lib/client/EditorPane.d.ts +13 -0
- package/lib/client/FlowEditor.d.ts +31 -0
- package/lib/client/InstanceList.d.ts +24 -0
- package/lib/client/NewInstanceDialog.d.ts +19 -0
- package/lib/client/PreviewPanel.d.ts +13 -0
- package/lib/client/RunView.d.ts +79 -0
- package/lib/client/StepChatPanel.d.ts +34 -0
- package/lib/client/TreeNav.d.ts +27 -0
- package/lib/client/UnifiedTimeline.d.ts +11 -0
- package/lib/client/WorkflowButton.d.ts +11 -0
- package/lib/client/WorkflowList.d.ts +18 -0
- package/lib/client/WorkflowOverlay.d.ts +6 -0
- package/lib/client/chat/AssistantMarkdown.d.ts +22 -0
- package/lib/client/chat/CompactionItem.d.ts +24 -0
- package/lib/client/chat/ContextBody.d.ts +136 -0
- package/lib/client/chat/ContextInjectionRow.d.ts +25 -0
- package/lib/client/chat/GenericCommandCard.d.ts +10 -0
- package/lib/client/chat/MessageIconActions.d.ts +41 -0
- package/lib/client/chat/MessageItem.d.ts +65 -0
- package/lib/client/chat/ReasoningRow.d.ts +13 -0
- package/lib/client/chat/TurnTailNodeView.d.ts +29 -0
- package/lib/client/chat/UserMessageRow.d.ts +18 -0
- package/lib/client/chat/attachment/ImageLightbox.d.ts +26 -0
- package/lib/client/chat/attachment/MessageImage.d.ts +56 -0
- package/lib/client/chat/attachment/image-attachment.d.ts +41 -0
- package/lib/client/chat/attachment/image-labels.d.ts +21 -0
- package/lib/client/chat/message-chrome.d.ts +51 -0
- package/lib/client/chat/reference/ReferenceIcon.d.ts +20 -0
- package/lib/client/chat/turn-assistant.d.ts +12 -0
- package/lib/client/chat/turn-metrics.d.ts +42 -0
- package/lib/client/chat/use-calendar-day.d.ts +5 -0
- package/lib/client/chat/use-throttled-visual-update.d.ts +7 -0
- package/lib/client/chat-flow.d.ts +110 -0
- package/lib/client/composer-selects.d.ts +17 -0
- package/lib/client/dag-editor-model.d.ts +141 -0
- package/lib/client/describe-error.d.ts +9 -0
- package/lib/client/index.d.ts +6 -0
- package/lib/client/locales.d.ts +777 -0
- package/lib/client/model-faces.d.ts +156 -0
- package/lib/client/open-discussion.d.ts +24 -0
- package/lib/client/panel-model.d.ts +34 -0
- package/lib/client/remote.d.ts +34 -0
- package/lib/client/run-view-model.d.ts +304 -0
- package/lib/client/sandore-save.d.ts +41 -0
- package/lib/client/sandore-sse.d.ts +15 -0
- package/lib/client/step-chat-retry.d.ts +44 -0
- package/lib/client/tool/GenericToolCard.d.ts +28 -0
- package/lib/client/tool/ToolRow.d.ts +95 -0
- package/lib/client/tool/diff-card-model.d.ts +63 -0
- package/lib/client/tool/read-card-model.d.ts +67 -0
- package/lib/client/tool/search-card-model.d.ts +94 -0
- package/lib/client/tool/terminal-card-model.d.ts +86 -0
- package/lib/client/tool/tool-call-model.d.ts +74 -0
- package/lib/client/tool/web-card-model.d.ts +44 -0
- package/lib/client/use-focus-refresh.d.ts +1 -0
- package/lib/client/workflow-face.d.ts +68 -0
- package/lib/client/workflow-store.d.ts +80 -0
- package/lib/client/workspace-path.d.ts +15 -0
- package/lib/client.js +38389 -0
- package/lib/index.js +8854 -0
- package/lib/microflow/branch-evaluator.d.ts +290 -0
- package/lib/microflow/concurrency-limiter.d.ts +39 -0
- package/lib/microflow/errors.d.ts +39 -0
- package/lib/microflow/file-utils.d.ts +210 -0
- package/lib/microflow/finalize-manager.d.ts +414 -0
- package/lib/microflow/fork-session.d.ts +151 -0
- package/lib/microflow/index.d.ts +17 -0
- package/lib/microflow/internal.d.ts +56 -0
- package/lib/microflow/loop-manager.d.ts +127 -0
- package/lib/microflow/rules-resolver.d.ts +28 -0
- package/lib/microflow/stage-executor.d.ts +362 -0
- package/lib/microflow/step-navigator.d.ts +108 -0
- package/lib/microflow/step-runner.d.ts +190 -0
- package/lib/microflow/subagents.d.ts +380 -0
- package/lib/tool/index.d.ts +65 -0
- package/lib/typert.host.d.ts +16 -0
- package/lib/typert.host.js +25 -0
- package/lib/typert.remote-client.d.ts +114 -0
- package/lib/typert.remote-client.js +399 -0
- package/lib/workflow/ask-tool.d.ts +78 -0
- package/lib/workflow/dag-navigator.d.ts +136 -0
- package/lib/workflow/domain.d.ts +304 -0
- package/lib/workflow/engine-surface.d.ts +70 -0
- package/lib/workflow/errors.d.ts +13 -0
- package/lib/workflow/host-agent.d.ts +53 -0
- package/lib/workflow/index.d.ts +23 -0
- package/lib/workflow/interaction-manager.d.ts +170 -0
- package/lib/workflow/lifecycle-manager.d.ts +111 -0
- package/lib/workflow/persistence.d.ts +35 -0
- package/lib/workflow/plugin.d.ts +27 -0
- package/lib/workflow/reactive-state.d.ts +27 -0
- package/lib/workflow/recovery-manager.d.ts +158 -0
- package/lib/workflow/remote.d.ts +139 -0
- package/lib/workflow/schema.d.ts +1917 -0
- package/lib/workflow/seam.d.ts +254 -0
- package/lib/workflow/shell.d.ts +127 -0
- package/lib/workflow/sse.d.ts +16 -0
- package/lib/workflow/typert/typert.host.d.ts +16 -0
- package/lib/workflow/typert/typert.remote-client.d.ts +114 -0
- package/lib/workflow/types.d.ts +187 -0
- package/lib/workflow/wire.d.ts +256 -0
- package/lib/workflow/workflow-engine.d.ts +373 -0
- package/lib/workflow/yaml-error.d.ts +22 -0
- package/lib/workflow/yaml-parser.d.ts +23 -0
- package/package.json +140 -0
- package/packages/client/CommandEditor.tsx +73 -0
- package/packages/client/DagEditor.module.css +711 -0
- package/packages/client/DagEditor.tsx +1203 -0
- package/packages/client/DefinitionDetail.module.css +110 -0
- package/packages/client/DefinitionDetail.tsx +126 -0
- package/packages/client/DefinitionList.tsx +173 -0
- package/packages/client/DiscussionInput.tsx +763 -0
- package/packages/client/DragHandle.tsx +92 -0
- package/packages/client/EditorPane.tsx +70 -0
- package/packages/client/FlowEditor.tsx +227 -0
- package/packages/client/InstanceList.tsx +250 -0
- package/packages/client/NewInstanceDialog.module.css +244 -0
- package/packages/client/NewInstanceDialog.tsx +478 -0
- package/packages/client/PreviewPanel.tsx +55 -0
- package/packages/client/RunView.module.css +246 -0
- package/packages/client/RunView.tsx +314 -0
- package/packages/client/StepChatPanel.tsx +486 -0
- package/packages/client/TreeNav.tsx +294 -0
- package/packages/client/UnifiedTimeline.tsx +484 -0
- package/packages/client/WorkflowButton.module.css +72 -0
- package/packages/client/WorkflowButton.tsx +50 -0
- package/packages/client/WorkflowList.tsx +158 -0
- package/packages/client/WorkflowOverlay.module.css +390 -0
- package/packages/client/WorkflowOverlay.tsx +136 -0
- package/packages/client/WorkflowPanel.module.css +247 -0
- package/packages/client/chat/AssistantMarkdown.module.css +38 -0
- package/packages/client/chat/AssistantMarkdown.tsx +120 -0
- package/packages/client/chat/ChatView.module.css +198 -0
- package/packages/client/chat/CompactionItem.tsx +85 -0
- package/packages/client/chat/ContextBody.module.css +162 -0
- package/packages/client/chat/ContextBody.tsx +598 -0
- package/packages/client/chat/ContextInjectionRow.module.css +63 -0
- package/packages/client/chat/ContextInjectionRow.tsx +78 -0
- package/packages/client/chat/GenericCommandCard.module.css +87 -0
- package/packages/client/chat/GenericCommandCard.tsx +76 -0
- package/packages/client/chat/MessageIconActions.module.css +84 -0
- package/packages/client/chat/MessageIconActions.tsx +147 -0
- package/packages/client/chat/MessageItem.module.css +293 -0
- package/packages/client/chat/MessageItem.tsx +384 -0
- package/packages/client/chat/ReasoningRow.module.css +82 -0
- package/packages/client/chat/ReasoningRow.tsx +70 -0
- package/packages/client/chat/TurnTailNodeView.module.css +10 -0
- package/packages/client/chat/TurnTailNodeView.tsx +63 -0
- package/packages/client/chat/UserMessageRow.module.css +75 -0
- package/packages/client/chat/UserMessageRow.tsx +92 -0
- package/packages/client/chat/accessibility.module.css +9 -0
- package/packages/client/chat/attachment/ImageLightbox.module.css +45 -0
- package/packages/client/chat/attachment/ImageLightbox.tsx +70 -0
- package/packages/client/chat/attachment/MessageImage.module.css +70 -0
- package/packages/client/chat/attachment/MessageImage.tsx +136 -0
- package/packages/client/chat/attachment/image-attachment.ts +98 -0
- package/packages/client/chat/attachment/image-labels.ts +34 -0
- package/packages/client/chat/message-chrome.ts +96 -0
- package/packages/client/chat/reference/ReferenceIcon.tsx +40 -0
- package/packages/client/chat/turn-assistant.ts +15 -0
- package/packages/client/chat/turn-metrics.ts +102 -0
- package/packages/client/chat/use-calendar-day.ts +27 -0
- package/packages/client/chat/use-throttled-visual-update.ts +44 -0
- package/packages/client/chat-flow.spec.ts +357 -0
- package/packages/client/chat-flow.ts +278 -0
- package/packages/client/composer-selects.module.css +376 -0
- package/packages/client/composer-selects.tsx +531 -0
- package/packages/client/dag-editor-model.spec.ts +173 -0
- package/packages/client/dag-editor-model.ts +359 -0
- package/packages/client/describe-error.spec.ts +18 -0
- package/packages/client/describe-error.ts +12 -0
- package/packages/client/index.ts +144 -0
- package/packages/client/input.module.css +854 -0
- package/packages/client/is-conditional-check.spec.ts +34 -0
- package/packages/client/locales.ts +789 -0
- package/packages/client/model-faces.ts +225 -0
- package/packages/client/open-discussion.spec.ts +39 -0
- package/packages/client/open-discussion.ts +32 -0
- package/packages/client/panel-model.spec.ts +152 -0
- package/packages/client/panel-model.ts +72 -0
- package/packages/client/preview.module.css +95 -0
- package/packages/client/react-dom.d.ts +11 -0
- package/packages/client/reactflow-theme.css +91 -0
- package/packages/client/remote.spec.ts +41 -0
- package/packages/client/remote.ts +46 -0
- package/packages/client/run-view-model.spec.ts +641 -0
- package/packages/client/run-view-model.ts +636 -0
- package/packages/client/sandore-save.spec.ts +146 -0
- package/packages/client/sandore-save.ts +97 -0
- package/packages/client/sandore-sse.ts +59 -0
- package/packages/client/step-chat-retry.spec.ts +70 -0
- package/packages/client/step-chat-retry.ts +65 -0
- package/packages/client/timeline.module.css +503 -0
- package/packages/client/tool/GenericToolCard.tsx +99 -0
- package/packages/client/tool/ToolRow.module.css +308 -0
- package/packages/client/tool/ToolRow.tsx +328 -0
- package/packages/client/tool/diff-card-model.ts +103 -0
- package/packages/client/tool/read-card-model.ts +86 -0
- package/packages/client/tool/search-card-model.ts +166 -0
- package/packages/client/tool/terminal-card-model.ts +225 -0
- package/packages/client/tool/tool-call-model.ts +267 -0
- package/packages/client/tool/web-card-model.ts +80 -0
- package/packages/client/tree-nav.module.css +282 -0
- package/packages/client/use-focus-refresh.ts +23 -0
- package/packages/client/workflow-face.ts +72 -0
- package/packages/client/workflow-store.spec.ts +49 -0
- package/packages/client/workflow-store.ts +161 -0
- package/packages/client/workspace-path.ts +27 -0
- package/packages/css-modules.d.ts +6 -0
- package/packages/microflow/branch-evaluator.spec.ts +745 -0
- package/packages/microflow/branch-evaluator.ts +1037 -0
- package/packages/microflow/concurrency-limiter.ts +70 -0
- package/packages/microflow/errors.ts +64 -0
- package/packages/microflow/file-utils.spec.ts +604 -0
- package/packages/microflow/file-utils.ts +521 -0
- package/packages/microflow/finalize-manager.spec.ts +1977 -0
- package/packages/microflow/finalize-manager.ts +1464 -0
- package/packages/microflow/fork-session.spec.ts +184 -0
- package/packages/microflow/fork-session.ts +316 -0
- package/packages/microflow/index.ts +17 -0
- package/packages/microflow/internal.ts +166 -0
- package/packages/microflow/loop-manager.spec.ts +294 -0
- package/packages/microflow/loop-manager.ts +255 -0
- package/packages/microflow/rules-resolver.spec.ts +147 -0
- package/packages/microflow/rules-resolver.ts +58 -0
- package/packages/microflow/stage-executor.spec.ts +1577 -0
- package/packages/microflow/stage-executor.ts +1478 -0
- package/packages/microflow/step-navigator.spec.ts +301 -0
- package/packages/microflow/step-navigator.ts +300 -0
- package/packages/microflow/step-runner.spec.ts +1132 -0
- package/packages/microflow/step-runner.ts +752 -0
- package/packages/microflow/subagents.spec.ts +215 -0
- package/packages/microflow/subagents.ts +422 -0
- package/packages/tests/fixtures/test-workflow.yaml +128 -0
- package/packages/tests/mocks/agents.ts +206 -0
- package/packages/tests/mocks/llm.ts +46 -0
- package/packages/tests/mocks/session-title.ts +31 -0
- package/packages/tests/mocks/sessions.ts +131 -0
- package/packages/tests/mocks/shell.ts +60 -0
- package/packages/tests/mocks/storage-domain.ts +101 -0
- package/packages/tests/mocks/subagents.ts +197 -0
- package/packages/tests/mocks/subprocess.ts +59 -0
- package/packages/tests/mocks/user-questions.ts +33 -0
- package/packages/tests/mocks/workspace.ts +52 -0
- package/packages/tool/index.spec.ts +287 -0
- package/packages/tool/index.ts +348 -0
- package/packages/workflow/ask-tool.ts +173 -0
- package/packages/workflow/dag-navigator.spec.ts +236 -0
- package/packages/workflow/dag-navigator.ts +425 -0
- package/packages/workflow/domain.ts +26 -0
- package/packages/workflow/engine-surface.ts +71 -0
- package/packages/workflow/errors.ts +13 -0
- package/packages/workflow/host-agent.spec.ts +92 -0
- package/packages/workflow/host-agent.ts +62 -0
- package/packages/workflow/index.ts +25 -0
- package/packages/workflow/interaction-manager.spec.ts +390 -0
- package/packages/workflow/interaction-manager.ts +397 -0
- package/packages/workflow/lifecycle-manager.spec.ts +683 -0
- package/packages/workflow/lifecycle-manager.ts +288 -0
- package/packages/workflow/persistence.spec.ts +109 -0
- package/packages/workflow/persistence.ts +64 -0
- package/packages/workflow/plugin.ts +39 -0
- package/packages/workflow/reactive-state.spec.ts +69 -0
- package/packages/workflow/reactive-state.ts +52 -0
- package/packages/workflow/recovery-manager.spec.ts +606 -0
- package/packages/workflow/recovery-manager.ts +471 -0
- package/packages/workflow/remote.spec.ts +1045 -0
- package/packages/workflow/remote.ts +479 -0
- package/packages/workflow/schema.spec.ts +303 -0
- package/packages/workflow/schema.ts +562 -0
- package/packages/workflow/seam.ts +293 -0
- package/packages/workflow/shell.ts +142 -0
- package/packages/workflow/sse.ts +110 -0
- package/packages/workflow/typert/typert.host.d.ts +16 -0
- package/packages/workflow/typert/typert.host.js +25 -0
- package/packages/workflow/typert/typert.remote-client.d.ts +114 -0
- package/packages/workflow/typert/typert.remote-client.js +399 -0
- package/packages/workflow/types.ts +258 -0
- package/packages/workflow/wire.ts +267 -0
- package/packages/workflow/workflow-engine-e2e.spec.ts +262 -0
- package/packages/workflow/workflow-engine-real-e2e.spec.ts +922 -0
- package/packages/workflow/workflow-engine.spec.ts +4389 -0
- package/packages/workflow/workflow-engine.ts +2133 -0
- package/packages/workflow/yaml-error.ts +29 -0
- package/packages/workflow/yaml-parser.spec.ts +757 -0
- package/packages/workflow/yaml-parser.ts +287 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* step-navigator.ts(StepNavigator)的单元测试:Step 级路由迁移回归。
|
|
3
|
+
*
|
|
4
|
+
* 验证 resolveNextNodes 从 workflow dag-navigator 迁入 microflow 后路由行为
|
|
5
|
+
* 与迁移前完全一致——并行/条件(批量 shouldExecute 分支 id 数组)/shell 匹配/循环
|
|
6
|
+
* (首入·回边·maxIter 硬上限·breakCondition 跳出·嵌套作用域归零)/无命中线性
|
|
7
|
+
* 兜底/跨 Stage 目标不路由(nodeIndex 拆分:microflow 只持当前 Stage Step 图)。
|
|
8
|
+
* @module dsh-sandore/microflow/src/step-navigator.spec
|
|
9
|
+
*/
|
|
10
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import type { LlmRuntime } from '@deepseek-ai/dsh-llm';
|
|
12
|
+
import type { SessionStore } from '@deepseek-ai/dsh-session';
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import type { WorkflowDefinition, WorkflowStage } from '../workflow/schema';
|
|
16
|
+
import { FakeLlm, textChunks } from '../tests/mocks/llm.ts';
|
|
17
|
+
import { FakeShell, successResult } from '../tests/mocks/shell.ts';
|
|
18
|
+
import { FakeSubagents } from '../tests/mocks/subagents.ts';
|
|
19
|
+
import { FakeSubprocess } from '../tests/mocks/subprocess.ts';
|
|
20
|
+
import { StepNavigator } from './step-navigator.ts';
|
|
21
|
+
import type { ResolveNextNodesContext } from './step-navigator.ts';
|
|
22
|
+
import type { LoopState } from './loop-manager.ts';
|
|
23
|
+
|
|
24
|
+
/** 组装测试 ctx(注入 FakeLlm + FakeShell + 占位 sessions)。 */
|
|
25
|
+
function makeCtx(): { ctx: Context; llm: FakeLlm; shell: FakeShell } {
|
|
26
|
+
const ctx = new Context();
|
|
27
|
+
const llm = new FakeLlm();
|
|
28
|
+
const shell = new FakeShell();
|
|
29
|
+
const subagents = new FakeSubagents();
|
|
30
|
+
const subprocess = new FakeSubprocess();
|
|
31
|
+
llm.providers.push({ id: 'p', name: 'Provider' });
|
|
32
|
+
llm.models.set('p', [{ provider: 'p', id: 'm', name: 'Model' }]);
|
|
33
|
+
ctx.provide('llm', llm as unknown as LlmRuntime);
|
|
34
|
+
ctx.provide('shell', shell);
|
|
35
|
+
ctx.provide('subagents', subagents);
|
|
36
|
+
ctx.provide('subprocess', subprocess);
|
|
37
|
+
ctx.provide('sessions', {} as unknown as SessionStore);
|
|
38
|
+
ctx.provide('fs', {});
|
|
39
|
+
return { ctx, llm, shell };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 构造单 Stage 工作流定义(steps 为流控位,完整定义在顶层 steps)。 */
|
|
43
|
+
function makeDef(stages: WorkflowStage[], steps: WorkflowDefinition['steps']): WorkflowDefinition {
|
|
44
|
+
return { id: 'wfl', name: 'wfl', version: '1', stages, steps: steps ?? [], settings: {} };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 组装调度上下文(visitCounts + 透传变量;不传 recentStepSessionId → fork 降级临时会话)。 */
|
|
48
|
+
function ctxWith(visitCounts: Record<string, number>): ResolveNextNodesContext {
|
|
49
|
+
return { visitCounts, passThroughVars: {} };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 新建 StepNavigator(绑定 stage + def,loopState 共享可变数组)。 */
|
|
53
|
+
function makeNavigator(
|
|
54
|
+
ctx: Context,
|
|
55
|
+
stage: WorkflowStage,
|
|
56
|
+
def: WorkflowDefinition,
|
|
57
|
+
loopState: LoopState = { activeLoopStack: [], activeLoopIterationStack: [] },
|
|
58
|
+
): StepNavigator {
|
|
59
|
+
return new StepNavigator(ctx, { defaultTimeoutMs: 60000 }, loopState, stage, def);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('StepNavigator Step 级路由迁移回归', () => {
|
|
63
|
+
describe('并行/线性兜底', () => {
|
|
64
|
+
it('无 branches → 数组顺序下一 Step(线性兜底)', async () => {
|
|
65
|
+
const { ctx } = makeCtx();
|
|
66
|
+
const stage: WorkflowStage = {
|
|
67
|
+
id: 'stg-1', name: 's1', steps: [{ id: 'step-a' }, { id: 'step-b' }],
|
|
68
|
+
};
|
|
69
|
+
const def = makeDef([stage], [{ id: 'step-a' }, { id: 'step-b' }]);
|
|
70
|
+
const nav = makeNavigator(ctx, stage, def);
|
|
71
|
+
const result = await nav.resolveNextNodes('step-a', ctxWith({}));
|
|
72
|
+
expect(result.nextNodeIds).toEqual(['step-b']);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('并行分支(多 next 无 condition)→ 全部纳入下一批', async () => {
|
|
76
|
+
const { ctx } = makeCtx();
|
|
77
|
+
const stage: WorkflowStage = {
|
|
78
|
+
id: 'stg-1', name: 's1',
|
|
79
|
+
steps: [{ id: 'step-a', branches: [{ next: 'step-b' }, { next: 'step-c' }] }, { id: 'step-b' }, { id: 'step-c' }],
|
|
80
|
+
};
|
|
81
|
+
const def = makeDef([stage], [{ id: 'step-a' }, { id: 'step-b' }, { id: 'step-c' }]);
|
|
82
|
+
const nav = makeNavigator(ctx, stage, def);
|
|
83
|
+
const result = await nav.resolveNextNodes('step-a', ctxWith({}));
|
|
84
|
+
expect(result.nextNodeIds).toEqual(['step-b', 'step-c']);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('跨 Stage 目标不进 Step 路由(nodeIndex 拆分:microflow 只持当前 Stage Step 图)', async () => {
|
|
88
|
+
const { ctx } = makeCtx();
|
|
89
|
+
// step-a 的并行分支 next 指向 stage-2 的 step-b(不在当前 Stage 索引)→ 不路由,
|
|
90
|
+
// 且 step-a 为当前 Stage 末位无数组后续 → 返回空(跨 Stage 由引擎 tryAdvanceStage 承接)。
|
|
91
|
+
const stage1: WorkflowStage = {
|
|
92
|
+
id: 'stg-1', name: 's1', steps: [{ id: 'step-a', branches: [{ next: 'step-b' }] }],
|
|
93
|
+
};
|
|
94
|
+
const stage2: WorkflowStage = { id: 'stg-2', name: 's2', steps: [{ id: 'step-b' }] };
|
|
95
|
+
const def = makeDef([stage1, stage2], [{ id: 'step-a' }, { id: 'step-b' }]);
|
|
96
|
+
const nav = makeNavigator(ctx, stage1, def);
|
|
97
|
+
const result = await nav.resolveNextNodes('step-a', ctxWith({}));
|
|
98
|
+
expect(result.nextNodeIds).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('条件分支(shouldExecute)', () => {
|
|
103
|
+
// step-a 条件分支目标 step-c(≠ 数组下一 step-b),便于区分「条件命中」与「线性兜底」。
|
|
104
|
+
const stage: WorkflowStage = {
|
|
105
|
+
id: 'stg-1', name: 's1',
|
|
106
|
+
steps: [{ id: 'step-a', branches: [{ condition: { type: 'prompt', value: { promptText: 'go-c?' } }, next: 'step-c' }] }, { id: 'step-b' }, { id: 'step-c' }],
|
|
107
|
+
};
|
|
108
|
+
const def = makeDef([stage], [{ id: 'step-a' }, { id: 'step-b' }, { id: 'step-c' }]);
|
|
109
|
+
|
|
110
|
+
it('shouldExecute 数组含目标 id → 走条件分支目标(非数组顺序)', async () => {
|
|
111
|
+
const { ctx, llm } = makeCtx();
|
|
112
|
+
llm.streamHandler = () => textChunks('{"shouldExecute": ["step-c"]}');
|
|
113
|
+
const nav = makeNavigator(ctx, stage, def);
|
|
114
|
+
const result = await nav.resolveNextNodes('step-a', ctxWith({}));
|
|
115
|
+
expect(result.nextNodeIds).toEqual(['step-c']);
|
|
116
|
+
expect(result.branchMeta?.result).toContain('shouldExecute');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('shouldExecute 空数组 → 跳过条件分支,线性兜底到数组下一 Step', async () => {
|
|
120
|
+
const { ctx, llm } = makeCtx();
|
|
121
|
+
llm.streamHandler = () => textChunks('{"shouldExecute": []}');
|
|
122
|
+
const nav = makeNavigator(ctx, stage, def);
|
|
123
|
+
const result = await nav.resolveNextNodes('step-a', ctxWith({}));
|
|
124
|
+
// 条件不命中 → 数组顺序兜底 step-b(而非条件目标 step-c)。
|
|
125
|
+
expect(result.nextNodeIds).toEqual(['step-b']);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('多条件分支一次批量评估:branchMeta 只写一次且全部命中分支路由', async () => {
|
|
129
|
+
const { ctx, llm } = makeCtx();
|
|
130
|
+
const multiStage: WorkflowStage = {
|
|
131
|
+
id: 'stg-1', name: 's1',
|
|
132
|
+
steps: [{
|
|
133
|
+
id: 'step-a',
|
|
134
|
+
branches: [
|
|
135
|
+
{ condition: { type: 'prompt', value: { promptText: 'go-c?' } }, next: 'step-c' },
|
|
136
|
+
{ condition: { type: 'prompt', value: { promptText: 'go-d?' } }, next: 'step-d' },
|
|
137
|
+
],
|
|
138
|
+
}, { id: 'step-b' }, { id: 'step-c' }, { id: 'step-d' }],
|
|
139
|
+
};
|
|
140
|
+
const multiDef = makeDef([multiStage], [{ id: 'step-a' }, { id: 'step-b' }, { id: 'step-c' }, { id: 'step-d' }]);
|
|
141
|
+
llm.streamHandler = () => textChunks('{"shouldExecute": ["step-c", "step-d"]}');
|
|
142
|
+
const nav = makeNavigator(ctx, multiStage, multiDef);
|
|
143
|
+
const result = await nav.resolveNextNodes('step-a', ctxWith({}));
|
|
144
|
+
// 批量一次 fork:两条分支全部命中(branchMeta 只写一次,不再被最后一条覆盖)。
|
|
145
|
+
expect(result.nextNodeIds).toEqual(['step-c', 'step-d']);
|
|
146
|
+
expect(result.branchMeta?.status).toBe('completed');
|
|
147
|
+
expect(result.branchMeta?.result).toContain('shouldExecute');
|
|
148
|
+
expect(llm.calls).toHaveLength(1);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('条件分支(shell matchesExpectation label)', () => {
|
|
153
|
+
const stage: WorkflowStage = {
|
|
154
|
+
id: 'stg-1', name: 's1',
|
|
155
|
+
steps: [{ id: 'step-a', branches: [{ condition: { type: 'shell', value: { command: 'echo ok' } }, label: 'ok', next: 'step-c' }] }, { id: 'step-b' }, { id: 'step-c' }],
|
|
156
|
+
};
|
|
157
|
+
const def = makeDef([stage], [{ id: 'step-a' }, { id: 'step-b' }, { id: 'step-c' }]);
|
|
158
|
+
|
|
159
|
+
it('shell stdout 匹配 label → 走条件分支目标', async () => {
|
|
160
|
+
const { ctx, shell } = makeCtx();
|
|
161
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
162
|
+
const nav = makeNavigator(ctx, stage, def);
|
|
163
|
+
const result = await nav.resolveNextNodes('step-a', ctxWith({}));
|
|
164
|
+
expect(result.nextNodeIds).toEqual(['step-c']);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('shell stdout 不匹配 label → 跳过,线性兜底', async () => {
|
|
168
|
+
const { ctx, shell } = makeCtx();
|
|
169
|
+
shell.runHandler = async spec => successResult(spec, 'no');
|
|
170
|
+
const nav = makeNavigator(ctx, stage, def);
|
|
171
|
+
const result = await nav.resolveNextNodes('step-a', ctxWith({}));
|
|
172
|
+
expect(result.nextNodeIds).toEqual(['step-b']);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('循环分支', () => {
|
|
177
|
+
// 单循环:step-loop(loop head)→ step-iter(回边目标)→ maxIter 后 exitTo step-done。
|
|
178
|
+
const stage: WorkflowStage = {
|
|
179
|
+
id: 'stg-1', name: 's1',
|
|
180
|
+
steps: [
|
|
181
|
+
{ id: 'step-loop', branches: [{ next: 'step-iter', loop: { maxIterations: 3, next: 'step-iter', exitTo: 'step-done' } }] },
|
|
182
|
+
{ id: 'step-iter', branches: [{ next: 'step-loop' }] },
|
|
183
|
+
{ id: 'step-done' },
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
const def = makeDef([stage], [{ id: 'step-loop' }, { id: 'step-iter' }, { id: 'step-done' }]);
|
|
187
|
+
|
|
188
|
+
it('首入循环(不在活跃循环)→ pushActiveLoop + 回边目标', async () => {
|
|
189
|
+
const { ctx } = makeCtx();
|
|
190
|
+
const loopState: LoopState = { activeLoopStack: [], activeLoopIterationStack: [] };
|
|
191
|
+
const nav = makeNavigator(ctx, stage, def, loopState);
|
|
192
|
+
const result = await nav.resolveNextNodes('step-loop', ctxWith({ 'step-loop': 0 }));
|
|
193
|
+
expect(result.nextNodeIds).toEqual(['step-iter']);
|
|
194
|
+
// 首入压栈:activeLoopNodeId = step-loop,迭代计数 = 1。
|
|
195
|
+
expect(nav.loopManagerInstance.activeLoopNodeId).toBe('step-loop');
|
|
196
|
+
expect(nav.loopManagerInstance.buildLoopIterationPath()).toEqual([1]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('回边(已在活跃循环)→ incrementLoopIteration + 回边目标', async () => {
|
|
200
|
+
const { ctx } = makeCtx();
|
|
201
|
+
const loopState: LoopState = { activeLoopStack: [], activeLoopIterationStack: [] };
|
|
202
|
+
const nav = makeNavigator(ctx, stage, def, loopState);
|
|
203
|
+
// 先首入(压栈 + 计数 1)。
|
|
204
|
+
await nav.resolveNextNodes('step-loop', ctxWith({ 'step-loop': 0 }));
|
|
205
|
+
// 回边:visitCount=1 < maxIter=3 → 继续。
|
|
206
|
+
const result = await nav.resolveNextNodes('step-loop', ctxWith({ 'step-loop': 1 }));
|
|
207
|
+
expect(result.nextNodeIds).toEqual(['step-iter']);
|
|
208
|
+
expect(nav.loopManagerInstance.buildLoopIterationPath()).toEqual([2]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('maxIterations 硬上限 → 跳出(reset + pop + exitTo)', async () => {
|
|
212
|
+
const { ctx } = makeCtx();
|
|
213
|
+
const loopState: LoopState = { activeLoopStack: [], activeLoopIterationStack: [] };
|
|
214
|
+
const visitCounts: Record<string, number> = {};
|
|
215
|
+
const nav = makeNavigator(ctx, stage, def, loopState);
|
|
216
|
+
// 驱动 3 次回边:首入(0) → 回边(1) → 回边(2) → 第 3 次回边 visitCount=3 >= maxIter=3 跳出。
|
|
217
|
+
await nav.resolveNextNodes('step-loop', { ...ctxWith(visitCounts), visitCounts: { 'step-loop': 0 } });
|
|
218
|
+
await nav.resolveNextNodes('step-loop', { ...ctxWith(visitCounts), visitCounts: { 'step-loop': 1 } });
|
|
219
|
+
await nav.resolveNextNodes('step-loop', { ...ctxWith(visitCounts), visitCounts: { 'step-loop': 2 } });
|
|
220
|
+
const result = await nav.resolveNextNodes('step-loop', ctxWith({ 'step-loop': 3 }));
|
|
221
|
+
expect(result.nextNodeIds).toEqual(['step-done']);
|
|
222
|
+
// 跳出后弹栈:无活跃循环。
|
|
223
|
+
expect(nav.loopManagerInstance.activeLoopNodeId).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('breakCondition shouldBreak=true → 跳出 exitTo(非 maxIter 硬上限)', async () => {
|
|
227
|
+
const { ctx, llm } = makeCtx();
|
|
228
|
+
const breakStage: WorkflowStage = {
|
|
229
|
+
id: 'stg-1', name: 's1',
|
|
230
|
+
steps: [
|
|
231
|
+
{ id: 'step-loop', branches: [{ next: 'step-iter', loop: { maxIterations: 5, breakCondition: { type: 'prompt', value: { promptText: 'done?' } }, next: 'step-iter', exitTo: 'step-done' } }] },
|
|
232
|
+
{ id: 'step-iter', branches: [{ next: 'step-loop' }] },
|
|
233
|
+
{ id: 'step-done' },
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
const breakDef = makeDef([breakStage], [{ id: 'step-loop' }, { id: 'step-iter' }, { id: 'step-done' }]);
|
|
237
|
+
const loopState: LoopState = { activeLoopStack: [], activeLoopIterationStack: [] };
|
|
238
|
+
const nav = makeNavigator(ctx, breakStage, breakDef, loopState);
|
|
239
|
+
// 首入(push)。
|
|
240
|
+
await nav.resolveNextNodes('step-loop', ctxWith({ 'step-loop': 0 }));
|
|
241
|
+
// 回边 visitCount=1 < maxIter=5,breakCondition 返回 shouldBreak=true → 跳出。
|
|
242
|
+
llm.streamHandler = () => textChunks('{"shouldBreak": true}');
|
|
243
|
+
const result = await nav.resolveNextNodes('step-loop', ctxWith({ 'step-loop': 1 }));
|
|
244
|
+
expect(result.nextNodeIds).toEqual(['step-done']);
|
|
245
|
+
expect(result.loopMeta?.result).toContain('shouldBreak');
|
|
246
|
+
expect(nav.loopManagerInstance.activeLoopNodeId).toBeNull();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('嵌套循环:内层跳出后归零内层作用域 visitCount,外层仍活跃', async () => {
|
|
250
|
+
const { ctx } = makeCtx();
|
|
251
|
+
const nestStage: WorkflowStage = {
|
|
252
|
+
id: 'stg-1', name: 's1',
|
|
253
|
+
steps: [
|
|
254
|
+
{ id: 'step-outer', branches: [{ next: 'step-inner', loop: { maxIterations: 2, next: 'step-inner', exitTo: 'step-done' } }] },
|
|
255
|
+
{ id: 'step-inner', branches: [{ next: 'step-iter', loop: { maxIterations: 1, next: 'step-iter', exitTo: 'step-outer' } }] },
|
|
256
|
+
{ id: 'step-iter', branches: [{ next: 'step-inner' }] },
|
|
257
|
+
{ id: 'step-done' },
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
const nestDef = makeDef([nestStage], [{ id: 'step-outer' }, { id: 'step-inner' }, { id: 'step-iter' }, { id: 'step-done' }]);
|
|
261
|
+
const loopState: LoopState = { activeLoopStack: [], activeLoopIterationStack: [] };
|
|
262
|
+
const nav = makeNavigator(ctx, nestStage, nestDef, loopState);
|
|
263
|
+
// 外层首入(push outer)。
|
|
264
|
+
await nav.resolveNextNodes('step-outer', ctxWith({ 'step-outer': 0 }));
|
|
265
|
+
expect(nav.loopManagerInstance.activeLoopNodeId).toBe('step-outer');
|
|
266
|
+
// 内层首入(push inner,迭代计数嵌套 [1,1])。
|
|
267
|
+
await nav.resolveNextNodes('step-inner', ctxWith({ 'step-inner': 0 }));
|
|
268
|
+
expect(nav.loopManagerInstance.buildLoopIterationPath()).toEqual([1, 1]);
|
|
269
|
+
// step-iter 执行后回边到 step-inner(visitCount 累加)。
|
|
270
|
+
await nav.resolveNextNodes('step-iter', ctxWith({ 'step-iter': 0 }));
|
|
271
|
+
// 内层回边 visitCount=1 >= maxIter=1 → 跳出,exitTo=step-outer。
|
|
272
|
+
const innerBreak = await nav.resolveNextNodes('step-inner', ctxWith({ 'step-inner': 1 }));
|
|
273
|
+
expect(innerBreak.nextNodeIds).toEqual(['step-outer']);
|
|
274
|
+
// 内层弹栈:外层仍活跃;内层作用域节点(step-iter)visitCount 归零。
|
|
275
|
+
expect(nav.loopManagerInstance.activeLoopNodeId).toBe('step-outer');
|
|
276
|
+
expect(nav.loopManagerInstance.buildLoopIterationPath()).toEqual([1]);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('节点边界', () => {
|
|
281
|
+
it('未知 nodeId → 返回空(不报错)', async () => {
|
|
282
|
+
const { ctx } = makeCtx();
|
|
283
|
+
const stage: WorkflowStage = { id: 'stg-1', name: 's1', steps: [{ id: 'step-a' }] };
|
|
284
|
+
const def = makeDef([stage], [{ id: 'step-a' }]);
|
|
285
|
+
const nav = makeNavigator(ctx, stage, def);
|
|
286
|
+
const result = await nav.resolveNextNodes('nope', ctxWith({}));
|
|
287
|
+
expect(result.nextNodeIds).toEqual([]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('loopManagerInstance 暴露共享循环管理器(供引擎/执行器 ID 后缀与迭代路径)', () => {
|
|
291
|
+
const { ctx } = makeCtx();
|
|
292
|
+
const stage: WorkflowStage = { id: 'stg-1', name: 's1', steps: [{ id: 'step-a' }] };
|
|
293
|
+
const def = makeDef([stage], [{ id: 'step-a' }]);
|
|
294
|
+
const loopState: LoopState = { activeLoopStack: [], activeLoopIterationStack: [] };
|
|
295
|
+
const nav = makeNavigator(ctx, stage, def, loopState);
|
|
296
|
+
// 同一实例:引擎从 stepNavigator.loopManagerInstance 取后注入 StageExecutor。
|
|
297
|
+
expect(nav.loopManagerInstance).toBe(nav.loopManagerInstance);
|
|
298
|
+
expect(nav.loopManagerInstance.activeLoopNodeId).toBeNull();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 内 Step 级 DAG 导航器(Task 3:Step 级 DAG 调度,迁自 workflow 的 dag-navigator)。
|
|
3
|
+
*
|
|
4
|
+
* **包职责边界(用户原则)**:Step 生命周期(含路由 resolveNextNodes + 执行 + 判断 + 收尾 +
|
|
5
|
+
* 循环计数)全归 microflow。本模块持有当前 Stage 的 Step 节点索引 `stepIndex`,以
|
|
6
|
+
* `resolveNextNodes()` 作为 Step 级调度单一入口,内部自建 {@link BranchEvaluator}(fork-based
|
|
7
|
+
* 条件评估)+ {@link LoopManager}(循环跳出/计数模型,缺陷 3 不动)承担 Step 级分支/循环路由。
|
|
8
|
+
* 一个 Stage 内部执行(含 Step 路由)自包含在 microflow 完成,才到下一 Stage(Stage→Stage 推进
|
|
9
|
+
* + Stage 级条件评估留 workflow 的 dag-navigator)。
|
|
10
|
+
*
|
|
11
|
+
* 构建 Stage 内 Step 节点索引 `stepIndex`(分离式合并:`stage.steps` 流控位 + 顶层
|
|
12
|
+
* `definition.steps` 完整定义,按 id 交叉引用)。**分支是唯一出边单元**(统一
|
|
13
|
+
* `branches: [{ next, condition?, loop? }]`):
|
|
14
|
+
* - 并行分支(无 `condition`)始终纳入下一批;
|
|
15
|
+
* - 条件分支(有 `condition`)一次批量评估(fork step 会话注入全量历史,AI 返回
|
|
16
|
+
* shouldExecute 分支 id 数组,shell 逐条走 `matchesExpectation(label)`);
|
|
17
|
+
* - 循环分支(有 `loop`,`next` 为回边)命中后经 `checkLoopBreak` 判定跳出(exitTo)或继续下一
|
|
18
|
+
* 迭代。无分支或无命中时按定义数组顺序线性兜底;跨 Stage 目标不在此路由(交由引擎
|
|
19
|
+
* `tryAdvanceStage` 跨阶段推进)。`branchMeta` / `loopMeta` 保留评估现场。
|
|
20
|
+
* @module dsh-sandore/microflow/src/step-navigator
|
|
21
|
+
*/
|
|
22
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
23
|
+
|
|
24
|
+
import { BranchEvaluator } from './branch-evaluator.ts';
|
|
25
|
+
import type { BranchEvalOptions, BranchEvaluatorConfig } from './branch-evaluator.ts';
|
|
26
|
+
import { LoopManager } from './loop-manager.ts';
|
|
27
|
+
import type { LoopState } from './loop-manager.ts';
|
|
28
|
+
|
|
29
|
+
import type { BranchMeta, DagBranch, WorkflowDefinition, WorkflowStage, WorkflowStep } from '../workflow/schema';
|
|
30
|
+
|
|
31
|
+
/** 分离式合并后的完整 Step(顶层定义 + 流控 branches)。 */
|
|
32
|
+
export type MergedStepRef = WorkflowStep & { readonly branches?: DagBranch[] | undefined };
|
|
33
|
+
|
|
34
|
+
/** Step 节点视图(出边统一在 branches;同 Stage 内节点,跨 Stage 目标不在此索引)。 */
|
|
35
|
+
export interface StepNodeRef {
|
|
36
|
+
/** 所属 Stage(始终可用,供数组顺序兜底与同 Stage 判定)。 */
|
|
37
|
+
readonly stage: WorkflowStage;
|
|
38
|
+
/** 合并后的完整 Step。 */
|
|
39
|
+
readonly step: MergedStepRef;
|
|
40
|
+
readonly id: string;
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly branches?: DagBranch[] | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `resolveNextNodes` 的调度上下文(由 StageExecutor.buildContext 注入的共享状态)。 */
|
|
46
|
+
export interface ResolveNextNodesContext {
|
|
47
|
+
/** 节点访问计数(循环节点记为回边次数,初始 0)。 */
|
|
48
|
+
readonly visitCounts: Record<string, number>;
|
|
49
|
+
/** 已解析的透传变量(`${xxx}` → 值)。 */
|
|
50
|
+
readonly passThroughVars: Readonly<Record<string, string>>;
|
|
51
|
+
readonly signal?: AbortSignal;
|
|
52
|
+
/**
|
|
53
|
+
* 当前 step 的子代理会话 ID(fork-based 判断:缺陷 2/4 改造)。
|
|
54
|
+
*
|
|
55
|
+
* 由 `stage-executor.buildContext` 从最近命令节点的 `sessionMeta.childId`
|
|
56
|
+
* (`synthetic === false` 真实子代理会话)取。条件分支 `evaluateConditions` 与
|
|
57
|
+
* 循环跳出 `checkLoopBreak` 消费:fork 此会话注入全量历史,AI 在 fork 会话上跑判断
|
|
58
|
+
* 即可见全部 I/O。合成 session(prompt/shell)或无已执行命令节点时为 undefined
|
|
59
|
+
* (判断降级为临时会话)。
|
|
60
|
+
*/
|
|
61
|
+
readonly recentStepSessionId?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `resolveNextNodes` 的返回:下一批节点 ID 与评估现场。 */
|
|
65
|
+
export interface ResolveNextNodesResult {
|
|
66
|
+
readonly nextNodeIds: string[];
|
|
67
|
+
readonly branchMeta?: BranchMeta;
|
|
68
|
+
readonly loopMeta?: BranchMeta;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 去重并保序。 */
|
|
72
|
+
function dedupe(ids: readonly string[]): string[] {
|
|
73
|
+
return [...new Set(ids)];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Stage 内 Step 级 DAG 导航器。
|
|
78
|
+
*
|
|
79
|
+
* 持有当前 Stage 的 `stepIndex`(stepId → 节点视图)、内置 {@link BranchEvaluator}/
|
|
80
|
+
* {@link LoopManager} 承担 Step 级分支/循环路由。循环节点 ID 后缀、迭代路径经
|
|
81
|
+
* {@link StepNavigator.loopManagerInstance} 暴露给 StageExecutor/引擎共享。
|
|
82
|
+
*/
|
|
83
|
+
export class StepNavigator {
|
|
84
|
+
private readonly stepIndex = new Map<string, StepNodeRef>();
|
|
85
|
+
private readonly evaluator: BranchEvaluator;
|
|
86
|
+
private readonly loopManager: LoopManager;
|
|
87
|
+
|
|
88
|
+
constructor(
|
|
89
|
+
private readonly ctx: Context,
|
|
90
|
+
config: BranchEvaluatorConfig,
|
|
91
|
+
loopState: LoopState,
|
|
92
|
+
stage: WorkflowStage,
|
|
93
|
+
workflowDef: WorkflowDefinition,
|
|
94
|
+
) {
|
|
95
|
+
this.buildStepIndex(stage, workflowDef);
|
|
96
|
+
this.evaluator = new BranchEvaluator(ctx, config);
|
|
97
|
+
this.loopManager = new LoopManager(ctx, config, loopState);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 循环管理器(StageExecutor/引擎共享,用于 ID 后缀与迭代路径)。 */
|
|
101
|
+
get loopManagerInstance(): LoopManager {
|
|
102
|
+
return this.loopManager;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Step 级调度单一入口。
|
|
107
|
+
*
|
|
108
|
+
* 统一分支路由(branches 是唯一出边单元):
|
|
109
|
+
* ① 并行分支(无 condition)始终纳入下一批(多条并发);
|
|
110
|
+
* ② 条件分支(有 condition)一次批量评估(AI 返回 shouldExecute 分支 id 数组;
|
|
111
|
+
* shell 走 `matchesExpectation(label)`),命中纳入;
|
|
112
|
+
* ③ 循环分支(有 loop,next 为回边):**首入与回边都调 `checkLoopBreak`**(只读
|
|
113
|
+
* visitCount,递增已由 `runLoop` 在处理节点前完成,等价基准 `processNode`
|
|
114
|
+
* L93-94 + L164);跳出走 `exitTo`(先 `resetLoopVisitCounts`+`popActiveLoop`),
|
|
115
|
+
* 继续回 `loop.next`(同循环 `incrementLoopIteration` / 首入 `pushActiveLoop`);
|
|
116
|
+
* ④ 无任何命中时按定义数组顺序线性兜底;无 Step 级目标返回空
|
|
117
|
+
* (由引擎 `tryAdvanceStage` 跨阶段推进)。
|
|
118
|
+
*/
|
|
119
|
+
async resolveNextNodes(nodeId: string, context: ResolveNextNodesContext): Promise<ResolveNextNodesResult> {
|
|
120
|
+
const node = this.stepIndex.get(nodeId);
|
|
121
|
+
if (node === undefined) {
|
|
122
|
+
return { nextNodeIds: [] };
|
|
123
|
+
}
|
|
124
|
+
const branches = node.branches ?? [];
|
|
125
|
+
if (branches.length === 0) {
|
|
126
|
+
const arrayNext = this.arrayNextStep(node);
|
|
127
|
+
return arrayNext === undefined ? { nextNodeIds: [] } : { nextNodeIds: [arrayNext] };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const parallel = branches.filter(branch => branch.condition === undefined && branch.loop === undefined);
|
|
131
|
+
const conditional = branches.filter(branch => branch.condition !== undefined && branch.loop === undefined);
|
|
132
|
+
const loopBranches = branches.filter(branch => branch.loop !== undefined);
|
|
133
|
+
|
|
134
|
+
const nextNodeIds: string[] = [];
|
|
135
|
+
let branchMeta: BranchMeta | undefined;
|
|
136
|
+
for (const branch of parallel) {
|
|
137
|
+
if (this.sameStageStep(node, branch.next) !== undefined) {
|
|
138
|
+
nextNodeIds.push(branch.next);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 条件分支:一次批量评估(Round 20 批量模板——AI 一次 fork 返回 shouldExecute 分支
|
|
143
|
+
// id 数组,shell 逐条匹配 label)。branchMeta 只写一次(修复多条件分支覆盖 bug:
|
|
144
|
+
// 旧逐条循环里 branchMeta 被最后一条覆盖,只保留最后一个分支的评估现场)。
|
|
145
|
+
if (conditional.length > 0) {
|
|
146
|
+
const { result, passedBranchIds } = await this.evaluator.evaluateConditions(
|
|
147
|
+
conditional.map(b => ({ branch: b, condition: b.condition! })),
|
|
148
|
+
this.branchEvalOptions(node, context),
|
|
149
|
+
);
|
|
150
|
+
branchMeta = {
|
|
151
|
+
status: 'completed',
|
|
152
|
+
result: result.output,
|
|
153
|
+
...(result.sessionHandle === null ? {} : { sessionId: result.sessionHandle }),
|
|
154
|
+
};
|
|
155
|
+
for (const nextId of passedBranchIds) {
|
|
156
|
+
if (this.sameStageStep(node, nextId) !== undefined) nextNodeIds.push(nextId);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 循环分支:对齐基准模型——checkLoopBreak 总是调(只读 visitCount,不递增,
|
|
161
|
+
// 递增已由 runLoop 在处理节点前完成,等价基准 processNode L93-94 + L164 总调)。
|
|
162
|
+
// 跳出时 resetLoopVisitCounts(归零该循环作用域)+ popActiveLoop + exitTo
|
|
163
|
+
// (仅当已在该循环回边时;首入即跳出无栈可弹);继续时同循环 incrementLoopIteration、
|
|
164
|
+
// 首入/嵌套 pushActiveLoop。
|
|
165
|
+
let loopResult: ResolveNextNodesResult | undefined;
|
|
166
|
+
for (const branch of loopBranches) {
|
|
167
|
+
const loop = branch.loop!;
|
|
168
|
+
const wasInThisLoop = this.loopManager.activeLoopNodeId === node.id;
|
|
169
|
+
// checkLoopBreak 只读 visitCount(不递增),根治命令门双重计数(缺陷 3 case ①)。
|
|
170
|
+
const visitCount = context.visitCounts[node.id] ?? 0;
|
|
171
|
+
// 缺陷 4(fork-based 改造):传入当前 step 会话 id,checkLoopBreak fork 注入全量历史
|
|
172
|
+
// (不手动传 I/O 文本——fork 会话携带循环体全部 I/O,AI 在 fork 会话上跑判断即可见)。
|
|
173
|
+
const outcome = await this.loopManager.checkLoopBreak(
|
|
174
|
+
{ id: node.id, loop },
|
|
175
|
+
visitCount,
|
|
176
|
+
{
|
|
177
|
+
...(context.recentStepSessionId === undefined ? {} : { stepSessionId: context.recentStepSessionId }),
|
|
178
|
+
stepId: node.id,
|
|
179
|
+
passThroughVars: context.passThroughVars,
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
if (outcome.shouldBreak) {
|
|
183
|
+
// 跳出:仅已在该循环(回边)才归零作用域 + 弹栈(首入即跳出无栈可弹)。
|
|
184
|
+
// 归零作用域推翻缺陷 22 的「进入时归零」,改用基准「跳出时归零」语义。
|
|
185
|
+
if (wasInThisLoop) {
|
|
186
|
+
this.loopManager.resetLoopVisitCounts(context.visitCounts);
|
|
187
|
+
this.loopManager.popActiveLoop();
|
|
188
|
+
}
|
|
189
|
+
const exitTo = loop.exitTo;
|
|
190
|
+
loopResult = {
|
|
191
|
+
nextNodeIds: exitTo !== undefined && this.sameStageStep(node, exitTo) !== undefined ? [exitTo] : [],
|
|
192
|
+
...(outcome.loopMeta === undefined ? {} : { loopMeta: outcome.loopMeta }),
|
|
193
|
+
};
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
// 继续:同循环 incrementLoopIteration;首入或嵌套 pushActiveLoop。
|
|
197
|
+
if (wasInThisLoop) {
|
|
198
|
+
this.loopManager.incrementLoopIteration();
|
|
199
|
+
} else {
|
|
200
|
+
this.loopManager.pushActiveLoop(node.id, loop.maxIterations);
|
|
201
|
+
}
|
|
202
|
+
const loopTo = loop.next;
|
|
203
|
+
if (loopTo !== undefined && this.sameStageStep(node, loopTo) !== undefined) {
|
|
204
|
+
// 缺陷 23:continue 分支同样透传 outcome.loopMeta(checkLoopBreak 在 shouldBreak=false
|
|
205
|
+
// 时也产出 loopMeta(completed),loop-manager.spec L174-175 实证)。修复前仅 break
|
|
206
|
+
// 分支透传,导致每次「继续」迭代的 breakCondition 评估现场静默丢失(审计/可观测性缺口)。
|
|
207
|
+
// 不改计数语义:visitCount 递增仍由 runLoop 单一完成(缺陷 3 已重构),路由条目仅
|
|
208
|
+
// 快照当前值——continue 透传不引入双重计数。
|
|
209
|
+
loopResult = {
|
|
210
|
+
nextNodeIds: [loopTo],
|
|
211
|
+
...(outcome.loopMeta === undefined ? {} : { loopMeta: outcome.loopMeta }),
|
|
212
|
+
};
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
// 回边目标无效(跨 Stage/缺失):回滚刚压栈后退出(仅 !wasInThisLoop 时刚 push)。
|
|
216
|
+
if (!wasInThisLoop) {
|
|
217
|
+
this.loopManager.resetLoopVisitCounts(context.visitCounts);
|
|
218
|
+
this.loopManager.popActiveLoop();
|
|
219
|
+
}
|
|
220
|
+
const exitTo = loop.exitTo;
|
|
221
|
+
// 缺陷 23:continue 分支回边目标无效的回滚路径同样透传 loopMeta(评估已完成)。
|
|
222
|
+
loopResult = {
|
|
223
|
+
nextNodeIds: exitTo !== undefined && this.sameStageStep(node, exitTo) !== undefined ? [exitTo] : [],
|
|
224
|
+
...(outcome.loopMeta === undefined ? {} : { loopMeta: outcome.loopMeta }),
|
|
225
|
+
};
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 循环命中时,循环路由优先于前进分支(循环接管本轮调度)。
|
|
230
|
+
if (loopResult !== undefined) {
|
|
231
|
+
return loopResult;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 无循环:前进分支 + 数组顺序兜底。
|
|
235
|
+
if (nextNodeIds.length > 0) {
|
|
236
|
+
return { nextNodeIds: dedupe(nextNodeIds), ...(branchMeta === undefined ? {} : { branchMeta }) };
|
|
237
|
+
}
|
|
238
|
+
const arrayNext = this.arrayNextStep(node);
|
|
239
|
+
if (arrayNext !== undefined) {
|
|
240
|
+
nextNodeIds.push(arrayNext);
|
|
241
|
+
}
|
|
242
|
+
return { nextNodeIds: dedupe(nextNodeIds), ...(branchMeta === undefined ? {} : { branchMeta }) };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 数组顺序的下一 Step(同 Stage 内,线性兜底)。 */
|
|
246
|
+
private arrayNextStep(node: StepNodeRef): string | undefined {
|
|
247
|
+
const steps = node.stage.steps;
|
|
248
|
+
const index = steps.findIndex(step => step.id === node.id);
|
|
249
|
+
return index < 0 ? undefined : steps[index + 1]?.id;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** 组装分支评估选项(候选分支 + 透传变量 + 循环上下文 + fork 源会话 + stepId + 取消信号)。 */
|
|
253
|
+
private branchEvalOptions(node: StepNodeRef, context: ResolveNextNodesContext): BranchEvalOptions {
|
|
254
|
+
const loopContext = this.loopManager.getLoopIterationContext();
|
|
255
|
+
return {
|
|
256
|
+
...(node.branches === undefined ? {} : { branches: node.branches }),
|
|
257
|
+
passThroughVars: context.passThroughVars,
|
|
258
|
+
loopTurn: loopContext.loopTurn,
|
|
259
|
+
loopMaxIterations: loopContext.loopMaxIterations,
|
|
260
|
+
...(context.recentStepSessionId === undefined ? {} : { stepSessionId: context.recentStepSessionId }),
|
|
261
|
+
stepId: node.id,
|
|
262
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** 目标节点是否为本 Stage 内的 Step(跨 Stage 目标返回 undefined,交由引擎跨阶段推进)。 */
|
|
267
|
+
private sameStageStep(node: StepNodeRef, targetId: string): StepNodeRef | undefined {
|
|
268
|
+
const target = this.stepIndex.get(targetId);
|
|
269
|
+
if (target === undefined) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
if (target.stage.id !== node.stage.id) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
return target;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 构建当前 Stage 的 Step 节点索引(分离式合并)。
|
|
280
|
+
*
|
|
281
|
+
* `stage.steps` 提供流控位(id + branches),`workflowDef.steps` 提供完整定义
|
|
282
|
+
* (command/映射/gates 等),按 id 合并成执行用的完整 Step。
|
|
283
|
+
*/
|
|
284
|
+
private buildStepIndex(stage: WorkflowStage, workflowDef: WorkflowDefinition): void {
|
|
285
|
+
const definitions = new Map((workflowDef.steps ?? []).map(step => [step.id, step] as const));
|
|
286
|
+
for (const flowStep of stage.steps) {
|
|
287
|
+
const definitionStep = definitions.get(flowStep.id);
|
|
288
|
+
const merged: MergedStepRef = definitionStep === undefined
|
|
289
|
+
? flowStep as MergedStepRef
|
|
290
|
+
: { ...definitionStep, ...(flowStep.branches === undefined ? {} : { branches: flowStep.branches }) };
|
|
291
|
+
this.stepIndex.set(flowStep.id, {
|
|
292
|
+
stage,
|
|
293
|
+
step: merged,
|
|
294
|
+
id: flowStep.id,
|
|
295
|
+
name: merged.name ?? flowStep.id,
|
|
296
|
+
branches: merged.branches,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|