@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,683 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lifecycle-manager.ts(LifecycleManager)与引擎接线的集成/单元测试:单步/全局
|
|
3
|
+
* 暂停与恢复、暂停登记表、幂等 no-op、非法操作与中止传播。
|
|
4
|
+
* @module dsh-sandore/workflow/src/lifecycle-manager.spec
|
|
5
|
+
*/
|
|
6
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
7
|
+
import type { LlmRuntime } from '@deepseek-ai/dsh-llm';
|
|
8
|
+
import type { SessionStore } from '@deepseek-ai/dsh-session';
|
|
9
|
+
import type { DomainFacility } from '@deepseek-ai/dsh-storage-domain';
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
|
|
12
|
+
import { assistantMessageEvent, FakeAgent, FakeAgentRegistry } from '../tests/mocks/agents.ts';
|
|
13
|
+
import { FakeLlm, textChunks } from '../tests/mocks/llm.ts';
|
|
14
|
+
import { FakeSessionStore } from '../tests/mocks/sessions.ts';
|
|
15
|
+
import { FakeSessionTitle } from '../tests/mocks/session-title.ts';
|
|
16
|
+
import { FakeShell, successResult } from '../tests/mocks/shell.ts';
|
|
17
|
+
import { MemoryStorageDomain } from '../tests/mocks/storage-domain.ts';
|
|
18
|
+
import { FakeSubagents } from '../tests/mocks/subagents.ts';
|
|
19
|
+
import { FakeSubprocess } from '../tests/mocks/subprocess.ts';
|
|
20
|
+
import { FakeUserQuestions } from '../tests/mocks/user-questions.ts';
|
|
21
|
+
import { SandoreEngineRuntime, SandoreError } from './index.ts';
|
|
22
|
+
import type { SandoreConfig, SandoreEngine, WorkflowTaskState } from './index.ts';
|
|
23
|
+
import { LifecycleManager } from './lifecycle-manager.ts';
|
|
24
|
+
import { SandorePersistence } from './persistence.ts';
|
|
25
|
+
import type { ShellExecutor } from './shell.ts';
|
|
26
|
+
import { parseWorkflowYaml } from './yaml-parser.ts';
|
|
27
|
+
|
|
28
|
+
/** 两 Step(shell → agent)定义:shell 立即完成、agent 子代理可挂起(暂停目标)。 */
|
|
29
|
+
const pauseYaml = `id: wfl-pause
|
|
30
|
+
name: pause
|
|
31
|
+
version: "1.0.0"
|
|
32
|
+
stages:
|
|
33
|
+
- id: stg-1
|
|
34
|
+
name: 处理
|
|
35
|
+
steps:
|
|
36
|
+
- id: step-shell
|
|
37
|
+
- id: step-agent
|
|
38
|
+
steps:
|
|
39
|
+
- id: step-shell
|
|
40
|
+
command:
|
|
41
|
+
type: shell
|
|
42
|
+
value:
|
|
43
|
+
command: "echo ok"
|
|
44
|
+
- id: step-agent
|
|
45
|
+
command:
|
|
46
|
+
type: agent
|
|
47
|
+
value:
|
|
48
|
+
promptText: "do-work"
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
/** 单 Step prompt 定义(prompt Step 为真实可续接子代理(决策 D3),用于暂停测试)。 */
|
|
52
|
+
const promptYaml = `id: wfl-prompt
|
|
53
|
+
name: prompt
|
|
54
|
+
version: "1.0.0"
|
|
55
|
+
stages:
|
|
56
|
+
- id: stg-1
|
|
57
|
+
name: 提示
|
|
58
|
+
steps:
|
|
59
|
+
- id: step-prompt
|
|
60
|
+
steps:
|
|
61
|
+
- id: step-prompt
|
|
62
|
+
command:
|
|
63
|
+
type: prompt
|
|
64
|
+
value:
|
|
65
|
+
promptText: "do it"
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
/** 写入一条工作流定义记录。 */
|
|
69
|
+
async function putDefinition(persistence: SandorePersistence, id: string, yaml: string): Promise<void> {
|
|
70
|
+
const definition = parseWorkflowYaml(yaml);
|
|
71
|
+
const createdAt = new Date().toISOString();
|
|
72
|
+
await persistence.definitions.put(id, {
|
|
73
|
+
yaml,
|
|
74
|
+
name: definition.name,
|
|
75
|
+
version: definition.version,
|
|
76
|
+
createdAt,
|
|
77
|
+
updatedAt: createdAt,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 组装集成测试 harness:注入内存 DSH 服务并加载引擎运行时。 */
|
|
82
|
+
async function harness(config: SandoreConfig = {}): Promise<{
|
|
83
|
+
ctx: Context;
|
|
84
|
+
engine: SandoreEngine;
|
|
85
|
+
persistence: SandorePersistence;
|
|
86
|
+
llm: FakeLlm;
|
|
87
|
+
shell: FakeShell;
|
|
88
|
+
sessions: FakeSessionStore;
|
|
89
|
+
subagents: FakeSubagents;
|
|
90
|
+
agents: FakeAgentRegistry;
|
|
91
|
+
userQuestions: FakeUserQuestions;
|
|
92
|
+
}> {
|
|
93
|
+
const ctx = new Context();
|
|
94
|
+
const storageDomain = new MemoryStorageDomain();
|
|
95
|
+
ctx.provide('storageDomain', storageDomain as unknown as DomainFacility);
|
|
96
|
+
await ctx.plugin(SandorePersistence);
|
|
97
|
+
const persistence = ctx.sandorePersistence;
|
|
98
|
+
|
|
99
|
+
const llm = new FakeLlm();
|
|
100
|
+
const shell = new FakeShell();
|
|
101
|
+
const sessions = new FakeSessionStore();
|
|
102
|
+
const agents = new FakeAgentRegistry(sessions);
|
|
103
|
+
const subagents = new FakeSubagents(ctx, (id) => agents.get(id));
|
|
104
|
+
const subprocess = new FakeSubprocess();
|
|
105
|
+
const userQuestions = new FakeUserQuestions();
|
|
106
|
+
ctx.provide('llm', llm as unknown as LlmRuntime);
|
|
107
|
+
ctx.provide('shell', shell as unknown as ShellExecutor);
|
|
108
|
+
ctx.provide('sessions', sessions as unknown as SessionStore);
|
|
109
|
+
ctx.provide('sessionPersistence', {});
|
|
110
|
+
ctx.provide('subagents', subagents);
|
|
111
|
+
ctx.provide('agents', agents);
|
|
112
|
+
ctx.provide('subprocess', subprocess);
|
|
113
|
+
ctx.provide('userQuestions', userQuestions);
|
|
114
|
+
ctx.provide('sessionTitle', new FakeSessionTitle());
|
|
115
|
+
ctx.provide('fs', {});
|
|
116
|
+
|
|
117
|
+
await ctx.plugin(SandoreEngineRuntime, config);
|
|
118
|
+
const engine = ctx.sandoreEngine;
|
|
119
|
+
return { ctx, engine, persistence, llm, shell, sessions, subagents, agents, userQuestions };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 配置 agent Step 可解析的 provider/model 目录(resolveProviderModel L3 兜底)。 */
|
|
123
|
+
function provideLlmDirectory(llm: FakeLlm): void {
|
|
124
|
+
llm.providers.push({ id: 'p', name: 'Provider' });
|
|
125
|
+
llm.models.set('p', [{ provider: 'p', id: 'm', name: 'Model' }]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 注册 pause/resume 相关 sandore/* 监听器并记录事件(名称 + 参数)。 */
|
|
129
|
+
function recordSandoreEvents(ctx: Context): Array<{ name: string; args: unknown[] }> {
|
|
130
|
+
const records: Array<{ name: string; args: unknown[] }> = [];
|
|
131
|
+
ctx.on('sandore/step-paused', (info, stepId, childId, name, loopIteration) => records.push({ name: 'sandore/step-paused', args: [info, stepId, childId, name, loopIteration] }));
|
|
132
|
+
ctx.on('sandore/step-resumed', (info, stepId, childId, resumePrompt, name, loopIteration) => records.push({ name: 'sandore/step-resumed', args: [info, stepId, childId, resumePrompt, name, loopIteration] }));
|
|
133
|
+
ctx.on('sandore/step-completed', (info, stepId, childId, output, name, loopIteration) => records.push({ name: 'sandore/step-completed', args: [info, stepId, childId, output, name, loopIteration] }));
|
|
134
|
+
ctx.on('sandore/step-failed', (info, stepId, childId, error, name, loopIteration) => records.push({ name: 'sandore/step-failed', args: [info, stepId, childId, error, name, loopIteration] }));
|
|
135
|
+
ctx.on('sandore/aborted', info => records.push({ name: 'sandore/aborted', args: [info] }));
|
|
136
|
+
return records;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 等待指定 stepId 的 `sandore/step-start`(确认该 Step 已开始执行)。 */
|
|
140
|
+
function waitForStepId(ctx: Context, stepId: string): Promise<void> {
|
|
141
|
+
return new Promise(resolve => {
|
|
142
|
+
const off = ctx.on('sandore/step-start', (_info, id) => {
|
|
143
|
+
if (id === stepId) {
|
|
144
|
+
off();
|
|
145
|
+
resolve();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 等待一次 `sandore/aborted`(以事件为信号)。 */
|
|
152
|
+
function waitForAborted(ctx: Context): Promise<void> {
|
|
153
|
+
return new Promise(resolve => {
|
|
154
|
+
ctx.once('sandore/aborted', () => resolve());
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 等待实例到达 completed/failed 终态(以 `sandore/end` 为信号)。 */
|
|
159
|
+
function waitForEnd(ctx: Context): Promise<void> {
|
|
160
|
+
return new Promise<void>(resolve => {
|
|
161
|
+
ctx.once('sandore/end', () => resolve());
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 等待子代理真实 childId 就绪(`onChildStarted` 把 running Step 的 sessionMeta 切为 real)。 */
|
|
166
|
+
async function waitForChildStarted(persistence: SandorePersistence, instanceId: string, stepId: string): Promise<void> {
|
|
167
|
+
await vi.waitFor(() => {
|
|
168
|
+
const step = persistence.state.get(instanceId)?.executedSteps.find(s => s.stepId === stepId);
|
|
169
|
+
expect(step?.sessionMeta?.synthetic).toBe(false);
|
|
170
|
+
expect(step?.sessionMeta?.childId).toBe('child-1');
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 组装最小完整状态快照(供 LifecycleManager 单元测试 readState 返回)。 */
|
|
175
|
+
function minimalState(overrides: Partial<WorkflowTaskState> = {}): WorkflowTaskState {
|
|
176
|
+
const createdAt = new Date().toISOString();
|
|
177
|
+
return {
|
|
178
|
+
taskId: 'inst-1',
|
|
179
|
+
jobId: 'job-1',
|
|
180
|
+
workflowId: 'wfl-pause',
|
|
181
|
+
cwd: '/tmp',
|
|
182
|
+
status: 'running',
|
|
183
|
+
originalInput: 'do it',
|
|
184
|
+
currentStageId: 'stg-1',
|
|
185
|
+
executedSteps: [],
|
|
186
|
+
stageInfos: [],
|
|
187
|
+
pendingInteractions: [],
|
|
188
|
+
pendingQueue: [],
|
|
189
|
+
visitCounts: {},
|
|
190
|
+
passThroughVars: {},
|
|
191
|
+
activeLoopIterationStack: [],
|
|
192
|
+
activeLoopNodeId: null,
|
|
193
|
+
activeLoopStack: [],
|
|
194
|
+
pausedChildIds: [],
|
|
195
|
+
stageContracts: {},
|
|
196
|
+
createdAt,
|
|
197
|
+
updatedAt: createdAt,
|
|
198
|
+
...overrides,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 挂起 agent Step 的公共准备:注册真实 childId + running 状态。settle 路径下子代理经
|
|
203
|
+
* `awaitSubagentSettlement` 挂起(pending `sandore/subagent-settled`),不再依赖 `whenIdle`。 */
|
|
204
|
+
function prepareHangingAgent(agents: FakeAgentRegistry, status: 'idle' | 'running' = 'running'): FakeAgent {
|
|
205
|
+
const child = new FakeAgent('child-1');
|
|
206
|
+
child.status = status;
|
|
207
|
+
agents.register(child);
|
|
208
|
+
return child;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 排空微任务:让 settle 事件 → collectSubagentOutput → stage-executor catch 传播完成。 */
|
|
212
|
+
function flushMicrotasks(): Promise<void> {
|
|
213
|
+
return new Promise(resolve => setTimeout(resolve, 0));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
describe('LifecycleManager pause/resume via engine', () => {
|
|
217
|
+
it('pauses a running step: ancestor interrupt + registry + paused status + persisted + double event', async () => {
|
|
218
|
+
const { ctx, engine, persistence, llm, shell, sessions, subagents, agents } = await harness();
|
|
219
|
+
await putDefinition(persistence, 'wfl-pause', pauseYaml);
|
|
220
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
221
|
+
provideLlmDirectory(llm);
|
|
222
|
+
const child = prepareHangingAgent(agents);
|
|
223
|
+
|
|
224
|
+
const records = recordSandoreEvents(ctx);
|
|
225
|
+
const stepStarted = waitForStepId(ctx, 'step-agent');
|
|
226
|
+
const run = await engine.startInstance({ workflowId: 'wfl-pause', originalInput: 'do it', cwd: '/tmp' });
|
|
227
|
+
await stepStarted;
|
|
228
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-agent');
|
|
229
|
+
|
|
230
|
+
await engine.pauseStep(run.instanceId, 'step-agent');
|
|
231
|
+
|
|
232
|
+
// interrupt 以 ancestor authority 调用,目标 childId = 'child-1'。
|
|
233
|
+
expect(subagents.interrupts).toHaveLength(1);
|
|
234
|
+
expect(subagents.interrupts[0]?.targetSessionId).toBe('child-1');
|
|
235
|
+
expect(subagents.interrupts[0]?.authority).toMatchObject({ kind: 'ancestor' });
|
|
236
|
+
|
|
237
|
+
const state = persistence.state.get(run.instanceId);
|
|
238
|
+
const paused = state?.executedSteps.find(step => step.stepId === 'step-agent');
|
|
239
|
+
expect(paused?.status).toBe('paused');
|
|
240
|
+
expect(paused?.pausedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
241
|
+
expect(state?.pausedChildIds).toEqual(['child-1']);
|
|
242
|
+
expect(state?.status).toBe('paused');
|
|
243
|
+
|
|
244
|
+
// sandore/step-paused 单发(Cordis 载荷含 childId;Round 25 去污染:宿主会话不再承载)。
|
|
245
|
+
const record = records.find(r => r.name === 'sandore/step-paused');
|
|
246
|
+
expect(record?.args[1]).toBe('step-agent');
|
|
247
|
+
expect(record?.args[2]).toBe('child-1');
|
|
248
|
+
expect(record?.args[3]).toBe('step-agent');
|
|
249
|
+
expect(record?.args[4]).toBeUndefined();
|
|
250
|
+
|
|
251
|
+
// 清理:让挂起的 Step settle 完成(pauseStep 的 interrupt 已 settle aborted 使 run 收敛为
|
|
252
|
+
// paused;此处显式 settle completed 令残余 awaiter 释放,避免悬挂 run)。
|
|
253
|
+
child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
|
|
254
|
+
subagents.settleChild('child-1', 'completed');
|
|
255
|
+
await flushMicrotasks();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('keeps a paused step paused (not failed) when interrupt settles aborted', async () => {
|
|
259
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
260
|
+
await putDefinition(persistence, 'wfl-pause', pauseYaml);
|
|
261
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
262
|
+
provideLlmDirectory(llm);
|
|
263
|
+
|
|
264
|
+
// settle 路径下子代理经 awaitSubagentSettlement 挂起;interrupt 自动 settle(aborted)。
|
|
265
|
+
const child = new FakeAgent('child-1');
|
|
266
|
+
child.status = 'running';
|
|
267
|
+
agents.register(child);
|
|
268
|
+
|
|
269
|
+
const records = recordSandoreEvents(ctx);
|
|
270
|
+
const stepStarted = waitForStepId(ctx, 'step-agent');
|
|
271
|
+
const run = await engine.startInstance({ workflowId: 'wfl-pause', originalInput: 'do it', cwd: '/tmp' });
|
|
272
|
+
await stepStarted;
|
|
273
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-agent');
|
|
274
|
+
|
|
275
|
+
await engine.pauseStep(run.instanceId, 'step-agent');
|
|
276
|
+
|
|
277
|
+
// 排空微任务:让 interrupt→settle(aborted)→STEP_PAUSED→stage-executor catch 传播完成。
|
|
278
|
+
await flushMicrotasks();
|
|
279
|
+
|
|
280
|
+
const state = persistence.state.get(run.instanceId);
|
|
281
|
+
const agent = state?.executedSteps.find(step => step.stepId === 'step-agent');
|
|
282
|
+
expect(agent?.status).toBe('paused');
|
|
283
|
+
expect(state?.status).toBe('paused');
|
|
284
|
+
expect(records.filter(record => record.name === 'sandore/step-failed')).toHaveLength(0);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('fails a step when the child settles with error (no matching pause) ', async () => {
|
|
288
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
289
|
+
await putDefinition(persistence, 'wfl-pause', pauseYaml);
|
|
290
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
291
|
+
provideLlmDirectory(llm);
|
|
292
|
+
|
|
293
|
+
const child = new FakeAgent('child-1');
|
|
294
|
+
child.status = 'running';
|
|
295
|
+
agents.register(child);
|
|
296
|
+
|
|
297
|
+
const records = recordSandoreEvents(ctx);
|
|
298
|
+
const stepStarted = waitForStepId(ctx, 'step-agent');
|
|
299
|
+
const run = await engine.startInstance({ workflowId: 'wfl-pause', originalInput: 'do it', cwd: '/tmp' });
|
|
300
|
+
await stepStarted;
|
|
301
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-agent');
|
|
302
|
+
|
|
303
|
+
// 未 pauseStep:子代理 settle(error)(非 aborted)→ collectSubagentOutput 抛 STEP_EXECUTION → failed。
|
|
304
|
+
subagents.settleChild('child-1', 'error');
|
|
305
|
+
await flushMicrotasks();
|
|
306
|
+
|
|
307
|
+
const state = persistence.state.get(run.instanceId);
|
|
308
|
+
expect(state?.status).toBe('failed');
|
|
309
|
+
expect(records.filter(record => record.name === 'sandore/step-failed')).toHaveLength(1);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('pauseStep is an idempotent no-op for an idle child (not marked paused)', async () => {
|
|
313
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
314
|
+
await putDefinition(persistence, 'wfl-pause', pauseYaml);
|
|
315
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
316
|
+
provideLlmDirectory(llm);
|
|
317
|
+
const child = prepareHangingAgent(agents, 'running');
|
|
318
|
+
|
|
319
|
+
const records = recordSandoreEvents(ctx);
|
|
320
|
+
const stepStarted = waitForStepId(ctx, 'step-agent');
|
|
321
|
+
const run = await engine.startInstance({ workflowId: 'wfl-pause', originalInput: 'do it', cwd: '/tmp' });
|
|
322
|
+
await stepStarted;
|
|
323
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-agent');
|
|
324
|
+
|
|
325
|
+
// settle 路径下 child 用 'running' 注册(避免 startContinuable 的 idle auto-settle
|
|
326
|
+
// 使 Step 提前 fail);pauseStep 前切 'idle' 模拟「子代理恰在 interrupt 前空闲」的
|
|
327
|
+
// 竞态窗口,验证 pauseStep 的 idle 幂等 no-op(不 interrupt、不误标 paused)。
|
|
328
|
+
child.status = 'idle';
|
|
329
|
+
await engine.pauseStep(run.instanceId, 'step-agent');
|
|
330
|
+
|
|
331
|
+
expect(subagents.interrupts).toHaveLength(0);
|
|
332
|
+
const state = persistence.state.get(run.instanceId);
|
|
333
|
+
expect(state?.pausedChildIds).toEqual([]);
|
|
334
|
+
const agent = state?.executedSteps.find(step => step.stepId === 'step-agent');
|
|
335
|
+
expect(agent?.status).toBe('running');
|
|
336
|
+
expect(records.some(r => r.name === 'sandore/step-paused')).toBe(false);
|
|
337
|
+
|
|
338
|
+
// 清理:idle child 未被 interrupt;显式 settle completed 令挂起 Step 收敛。
|
|
339
|
+
child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
|
|
340
|
+
subagents.settleChild('child-1', 'completed');
|
|
341
|
+
await flushMicrotasks();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('pauseStep throws INVALID_OPERATION for non-existent and terminal steps', async () => {
|
|
345
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
346
|
+
await putDefinition(persistence, 'wfl-pause', pauseYaml);
|
|
347
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
348
|
+
provideLlmDirectory(llm);
|
|
349
|
+
const child = prepareHangingAgent(agents);
|
|
350
|
+
const stepStarted = waitForStepId(ctx, 'step-agent');
|
|
351
|
+
const run = await engine.startInstance({ workflowId: 'wfl-pause', originalInput: 'do it', cwd: '/tmp' });
|
|
352
|
+
await stepStarted;
|
|
353
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-agent');
|
|
354
|
+
|
|
355
|
+
await expect(engine.pauseStep(run.instanceId, 'step-nonexistent')).rejects.toMatchObject({ code: 'INVALID_OPERATION' });
|
|
356
|
+
await expect(engine.pauseStep(run.instanceId, 'step-shell')).rejects.toMatchObject({ code: 'INVALID_OPERATION' });
|
|
357
|
+
|
|
358
|
+
child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
|
|
359
|
+
subagents.settleChild('child-1', 'completed');
|
|
360
|
+
await flushMicrotasks();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('resumes a registered step: followup hostAgent + removed from registry + running + double event', async () => {
|
|
364
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
365
|
+
await putDefinition(persistence, 'wfl-pause', pauseYaml);
|
|
366
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
367
|
+
provideLlmDirectory(llm);
|
|
368
|
+
const child = prepareHangingAgent(agents);
|
|
369
|
+
|
|
370
|
+
const records = recordSandoreEvents(ctx);
|
|
371
|
+
const stepStarted = waitForStepId(ctx, 'step-agent');
|
|
372
|
+
const run = await engine.startInstance({ workflowId: 'wfl-pause', originalInput: 'do it', cwd: '/tmp' });
|
|
373
|
+
await stepStarted;
|
|
374
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-agent');
|
|
375
|
+
|
|
376
|
+
await engine.pauseStep(run.instanceId, 'step-agent');
|
|
377
|
+
await engine.resumeStep(run.instanceId, 'step-agent', '请继续处理 X');
|
|
378
|
+
|
|
379
|
+
// followup 以 hostAgent(id = instanceId)为 parent 发送提示。
|
|
380
|
+
expect(subagents.followups).toHaveLength(1);
|
|
381
|
+
expect(subagents.followups[0]?.childId).toBe('child-1');
|
|
382
|
+
expect(subagents.followups[0]?.parent.id).toBe(run.instanceId);
|
|
383
|
+
expect(subagents.followups[0]?.content).toEqual([{ type: 'text', text: '请继续处理 X' }]);
|
|
384
|
+
|
|
385
|
+
const state = persistence.state.get(run.instanceId);
|
|
386
|
+
// 段模型:resumeStep 只负责 followup/移表/消息/事件,不建段——被暂停段保持 paused
|
|
387
|
+
// 定格快照,新执行段由执行器续跑(resumePendingPaused)时创建。
|
|
388
|
+
const resumed = state?.executedSteps.find(step => step.stepId === 'step-agent');
|
|
389
|
+
expect(resumed?.status).toBe('paused');
|
|
390
|
+
expect(resumed?.pausedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
391
|
+
expect(state?.pausedChildIds).toEqual([]);
|
|
392
|
+
expect(state?.status).toBe('running');
|
|
393
|
+
|
|
394
|
+
// sandore/step-resumed 双发(载荷含 childId 与 resumePrompt)。
|
|
395
|
+
const record = records.find(r => r.name === 'sandore/step-resumed');
|
|
396
|
+
expect(record?.args[1]).toBe('step-agent');
|
|
397
|
+
expect(record?.args[2]).toBe('child-1');
|
|
398
|
+
expect(record?.args[3]).toBe('请继续处理 X');
|
|
399
|
+
expect(record?.args[4]).toBe('step-agent');
|
|
400
|
+
|
|
401
|
+
child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
|
|
402
|
+
subagents.settleChild('child-1', 'completed');
|
|
403
|
+
await flushMicrotasks();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('resumeStep throws INVALID_OPERATION for a non-paused step', async () => {
|
|
407
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
408
|
+
await putDefinition(persistence, 'wfl-pause', pauseYaml);
|
|
409
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
410
|
+
provideLlmDirectory(llm);
|
|
411
|
+
const child = prepareHangingAgent(agents);
|
|
412
|
+
const stepStarted = waitForStepId(ctx, 'step-agent');
|
|
413
|
+
const run = await engine.startInstance({ workflowId: 'wfl-pause', originalInput: 'do it', cwd: '/tmp' });
|
|
414
|
+
await stepStarted;
|
|
415
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-agent');
|
|
416
|
+
|
|
417
|
+
await expect(engine.resumeStep(run.instanceId, 'step-agent')).rejects.toMatchObject({ code: 'INVALID_OPERATION' });
|
|
418
|
+
|
|
419
|
+
child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
|
|
420
|
+
subagents.settleChild('child-1', 'completed');
|
|
421
|
+
await flushMicrotasks();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('pauseAllSteps pauses only running steps and leaves completed untouched', async () => {
|
|
425
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
426
|
+
await putDefinition(persistence, 'wfl-pause', pauseYaml);
|
|
427
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
428
|
+
provideLlmDirectory(llm);
|
|
429
|
+
const child = prepareHangingAgent(agents);
|
|
430
|
+
const stepStarted = waitForStepId(ctx, 'step-agent');
|
|
431
|
+
const run = await engine.startInstance({ workflowId: 'wfl-pause', originalInput: 'do it', cwd: '/tmp' });
|
|
432
|
+
await stepStarted;
|
|
433
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-agent');
|
|
434
|
+
|
|
435
|
+
await engine.pauseAllSteps(run.instanceId);
|
|
436
|
+
|
|
437
|
+
const state = persistence.state.get(run.instanceId);
|
|
438
|
+
const shellStep = state?.executedSteps.find(step => step.stepId === 'step-shell');
|
|
439
|
+
const agentStep = state?.executedSteps.find(step => step.stepId === 'step-agent');
|
|
440
|
+
expect(shellStep?.status).toBe('completed');
|
|
441
|
+
expect(agentStep?.status).toBe('paused');
|
|
442
|
+
expect(state?.pausedChildIds).toEqual(['child-1']);
|
|
443
|
+
expect(subagents.interrupts).toHaveLength(1);
|
|
444
|
+
|
|
445
|
+
child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
|
|
446
|
+
subagents.settleChild('child-1', 'completed');
|
|
447
|
+
await flushMicrotasks();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('resumeAllSteps follows up each registered child and clears the registry', async () => {
|
|
451
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
452
|
+
await putDefinition(persistence, 'wfl-pause', pauseYaml);
|
|
453
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
454
|
+
provideLlmDirectory(llm);
|
|
455
|
+
const child = prepareHangingAgent(agents);
|
|
456
|
+
const stepStarted = waitForStepId(ctx, 'step-agent');
|
|
457
|
+
const run = await engine.startInstance({ workflowId: 'wfl-pause', originalInput: 'do it', cwd: '/tmp' });
|
|
458
|
+
await stepStarted;
|
|
459
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-agent');
|
|
460
|
+
|
|
461
|
+
await engine.pauseAllSteps(run.instanceId);
|
|
462
|
+
await engine.resumeAllSteps(run.instanceId, '全部继续');
|
|
463
|
+
|
|
464
|
+
const state = persistence.state.get(run.instanceId);
|
|
465
|
+
expect(state?.pausedChildIds).toEqual([]);
|
|
466
|
+
expect(state?.status).toBe('running');
|
|
467
|
+
expect(subagents.followups).toHaveLength(1);
|
|
468
|
+
expect(subagents.followups[0]?.childId).toBe('child-1');
|
|
469
|
+
expect(subagents.followups[0]?.content).toEqual([{ type: 'text', text: '全部继续' }]);
|
|
470
|
+
|
|
471
|
+
child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
|
|
472
|
+
subagents.settleChild('child-1', 'completed');
|
|
473
|
+
await flushMicrotasks();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('abortInstance interrupts registered children, marks aborted, cleans up, and rejects repeat', async () => {
|
|
477
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
478
|
+
await putDefinition(persistence, 'wfl-pause', pauseYaml);
|
|
479
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
480
|
+
provideLlmDirectory(llm);
|
|
481
|
+
prepareHangingAgent(agents);
|
|
482
|
+
const stepStarted = waitForStepId(ctx, 'step-agent');
|
|
483
|
+
const run = await engine.startInstance({ workflowId: 'wfl-pause', originalInput: 'do it', cwd: '/tmp' });
|
|
484
|
+
await stepStarted;
|
|
485
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-agent');
|
|
486
|
+
|
|
487
|
+
await engine.pauseStep(run.instanceId, 'step-agent');
|
|
488
|
+
const interruptsBefore = subagents.interrupts.length;
|
|
489
|
+
const aborted = waitForAborted(ctx);
|
|
490
|
+
await engine.abortInstance(run.instanceId);
|
|
491
|
+
await aborted;
|
|
492
|
+
|
|
493
|
+
// 中止传播到登记表内全部子代理(pause 已 interrupt 一次,abort 再 interrupt 一次)。
|
|
494
|
+
expect(subagents.interrupts.length).toBe(interruptsBefore + 1);
|
|
495
|
+
const abortInterrupt = subagents.interrupts[subagents.interrupts.length - 1];
|
|
496
|
+
expect(abortInterrupt?.targetSessionId).toBe('child-1');
|
|
497
|
+
expect(abortInterrupt?.authority).toMatchObject({ kind: 'ancestor' });
|
|
498
|
+
|
|
499
|
+
expect(persistence.state.get(run.instanceId)?.status).toBe('aborted');
|
|
500
|
+
expect(engine.getState(run.instanceId).status).toBe('aborted');
|
|
501
|
+
await expect(engine.abortInstance(run.instanceId)).rejects.toMatchObject({ code: 'INVALID_OPERATION' });
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('abort during a paused wait converges the main loop (no hang) with a single aborted emission', async () => {
|
|
505
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
506
|
+
await putDefinition(persistence, 'wfl-pause', pauseYaml);
|
|
507
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
508
|
+
provideLlmDirectory(llm);
|
|
509
|
+
|
|
510
|
+
const child = new FakeAgent('child-1');
|
|
511
|
+
child.status = 'running';
|
|
512
|
+
agents.register(child);
|
|
513
|
+
// settle 路径下:pauseStep 的 interrupt 自动 settle(aborted) → STEP_PAUSED →
|
|
514
|
+
// stage-executor 判定 paused 并驱动主循环进入 waitForResume 等待。
|
|
515
|
+
|
|
516
|
+
// 捕获 execute 异步任务的完成 Promise,用于断言主循环收敛(不悬挂)。
|
|
517
|
+
let executeSettled = false;
|
|
518
|
+
const runtime = engine as unknown as { execute: (instanceId: string) => Promise<void> };
|
|
519
|
+
const originalExecute = runtime.execute.bind(runtime);
|
|
520
|
+
vi.spyOn(runtime, 'execute').mockImplementation((instanceId: string) => {
|
|
521
|
+
const done = originalExecute(instanceId);
|
|
522
|
+
void done.then(() => {
|
|
523
|
+
executeSettled = true;
|
|
524
|
+
});
|
|
525
|
+
return done;
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const records = recordSandoreEvents(ctx);
|
|
529
|
+
const stepStarted = waitForStepId(ctx, 'step-agent');
|
|
530
|
+
const run = await engine.startInstance({ workflowId: 'wfl-pause', originalInput: 'do it', cwd: '/tmp' });
|
|
531
|
+
await stepStarted;
|
|
532
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-agent');
|
|
533
|
+
|
|
534
|
+
await engine.pauseStep(run.instanceId, 'step-agent');
|
|
535
|
+
// 排空微任务:让 interrupt→settle(aborted)→STEP_PAUSED 传播到 stage-executor 并判为 paused,
|
|
536
|
+
// 主循环随后进入 waitForResume 等待。
|
|
537
|
+
await flushMicrotasks();
|
|
538
|
+
|
|
539
|
+
const pausedState = persistence.state.get(run.instanceId);
|
|
540
|
+
expect(pausedState?.executedSteps.find(s => s.stepId === 'step-agent')?.status).toBe('paused');
|
|
541
|
+
expect(pausedState?.status).toBe('paused');
|
|
542
|
+
|
|
543
|
+
const aborted = waitForAborted(ctx);
|
|
544
|
+
await engine.abortInstance(run.instanceId);
|
|
545
|
+
await aborted;
|
|
546
|
+
|
|
547
|
+
// 主循环收敛:waitForResume 被 abort 唤醒,execute 异步任务正常返回(不悬挂)。
|
|
548
|
+
await vi.waitFor(() => {
|
|
549
|
+
expect(executeSettled).toBe(true);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// sandore/aborted 仍恰好一次(abortInstance 显式发射,主循环 aborted 兜底不双发)。
|
|
553
|
+
expect(records.filter(r => r.name === 'sandore/aborted')).toHaveLength(1);
|
|
554
|
+
// 实例终态 aborted。
|
|
555
|
+
expect(persistence.state.get(run.instanceId)?.status).toBe('aborted');
|
|
556
|
+
expect(engine.getState(run.instanceId).status).toBe('aborted');
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
describe('LifecycleManager pause/resume full loop', () => {
|
|
561
|
+
it('resumes a paused subagent step to completed and advances the DAG (sandore/step-completed)', async () => {
|
|
562
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
563
|
+
await putDefinition(persistence, 'wfl-pause', pauseYaml);
|
|
564
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
565
|
+
provideLlmDirectory(llm);
|
|
566
|
+
|
|
567
|
+
const child = new FakeAgent('child-1');
|
|
568
|
+
child.status = 'running';
|
|
569
|
+
agents.register(child);
|
|
570
|
+
// settle 路径:pauseStep → interrupt 自动 settle(aborted) → paused;resume → followup
|
|
571
|
+
// (cold-resume) → 测试 settleChild(completed) 令续跑收敛,最终输出由 child.session 注入。
|
|
572
|
+
|
|
573
|
+
const records = recordSandoreEvents(ctx);
|
|
574
|
+
const stepStarted = waitForStepId(ctx, 'step-agent');
|
|
575
|
+
const ended = waitForEnd(ctx);
|
|
576
|
+
const run = await engine.startInstance({ workflowId: 'wfl-pause', originalInput: 'do it', cwd: '/tmp' });
|
|
577
|
+
await stepStarted;
|
|
578
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-agent');
|
|
579
|
+
|
|
580
|
+
await engine.pauseStep(run.instanceId, 'step-agent');
|
|
581
|
+
// 排空微任务:让 interrupt→settle(aborted)→STEP_PAUSED 传播并判为 paused。
|
|
582
|
+
await flushMicrotasks();
|
|
583
|
+
|
|
584
|
+
const pausedState = persistence.state.get(run.instanceId);
|
|
585
|
+
expect(pausedState?.executedSteps.find(s => s.stepId === 'step-agent')?.status).toBe('paused');
|
|
586
|
+
|
|
587
|
+
// 注入最终输出后 resume:Step 应续跑到 completed 并推进 DAG 至实例 completed。
|
|
588
|
+
child.session.append(assistantMessageEvent([{ type: 'text', text: 'resumed output' }]));
|
|
589
|
+
await engine.resumeStep(run.instanceId, 'step-agent', '请继续');
|
|
590
|
+
// resume 的 followup(cold-resume)后 settle completed 令续跑收敛。
|
|
591
|
+
subagents.settleChild('child-1', 'completed');
|
|
592
|
+
|
|
593
|
+
await ended;
|
|
594
|
+
|
|
595
|
+
const state = persistence.state.get(run.instanceId);
|
|
596
|
+
expect(state?.status).toBe('completed');
|
|
597
|
+
// 段模型:resumeStep 后 executor 续跑创建新段(attemptIndex 1),旧 paused 段保持定格。
|
|
598
|
+
const agentSteps = state?.executedSteps.filter(s => s.stepId === 'step-agent') ?? [];
|
|
599
|
+
expect(agentSteps).toHaveLength(2);
|
|
600
|
+
expect(agentSteps[0]?.status).toBe('paused');
|
|
601
|
+
const resumedSegment = agentSteps.sort((a, b) => (b.attemptIndex ?? 0) - (a.attemptIndex ?? 0))[0];
|
|
602
|
+
expect(resumedSegment?.status).toBe('completed');
|
|
603
|
+
expect(resumedSegment?.output).toEqual([{ type: 'text', text: 'resumed output' }]);
|
|
604
|
+
expect(state?.stageInfos[0]?.status).toBe('completed');
|
|
605
|
+
|
|
606
|
+
const completed = records.find(r => r.name === 'sandore/step-completed' && r.args[1] === 'step-agent');
|
|
607
|
+
expect(completed?.args[2]).toBe('child-1');
|
|
608
|
+
expect(completed?.args[3]).toEqual([{ type: 'text', text: 'resumed output' }]);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
describe('LifecycleManager non-subagent pause guard', () => {
|
|
613
|
+
it('pauses a running prompt step (决策 D3: prompt 为真实子代理,可暂停)', async () => {
|
|
614
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
615
|
+
await putDefinition(persistence, 'wfl-prompt', promptYaml);
|
|
616
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
617
|
+
provideLlmDirectory(llm);
|
|
618
|
+
// prompt Step 走可续接子代理:child status='running' 不自动 settle
|
|
619
|
+
// (collectSubagentOutput 挂起等待),保持 step-prompt running 供暂停测试。
|
|
620
|
+
const child = new FakeAgent('child-1');
|
|
621
|
+
child.status = 'running';
|
|
622
|
+
agents.register(child);
|
|
623
|
+
|
|
624
|
+
const stepStarted = waitForStepId(ctx, 'step-prompt');
|
|
625
|
+
const run = await engine.startInstance({ workflowId: 'wfl-prompt', originalInput: 'do it', cwd: '/tmp' });
|
|
626
|
+
await stepStarted;
|
|
627
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-prompt');
|
|
628
|
+
|
|
629
|
+
// prompt 与 agent/skill/mcp 同为真实子代理:pauseStep 应正常 interrupt + 登记 + paused。
|
|
630
|
+
await engine.pauseStep(run.instanceId, 'step-prompt');
|
|
631
|
+
await flushMicrotasks();
|
|
632
|
+
|
|
633
|
+
const state = persistence.state.get(run.instanceId);
|
|
634
|
+
expect(state?.executedSteps.find(s => s.stepId === 'step-prompt')?.status).toBe('paused');
|
|
635
|
+
expect(state?.pausedChildIds).toEqual(['child-1']);
|
|
636
|
+
expect(subagents.interrupts).toHaveLength(1);
|
|
637
|
+
expect(subagents.interrupts[0]?.targetSessionId).toBe('child-1');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('pauseAllSteps pauses a running prompt step (决策 D3)', async () => {
|
|
641
|
+
const { ctx, engine, persistence, llm, shell, subagents, agents } = await harness();
|
|
642
|
+
await putDefinition(persistence, 'wfl-prompt', promptYaml);
|
|
643
|
+
shell.runHandler = async spec => successResult(spec, 'ok');
|
|
644
|
+
provideLlmDirectory(llm);
|
|
645
|
+
// prompt Step 走可续接子代理:child status='running' 不自动 settle,保持 running。
|
|
646
|
+
const child = new FakeAgent('child-1');
|
|
647
|
+
child.status = 'running';
|
|
648
|
+
agents.register(child);
|
|
649
|
+
|
|
650
|
+
const stepStarted = waitForStepId(ctx, 'step-prompt');
|
|
651
|
+
const run = await engine.startInstance({ workflowId: 'wfl-prompt', originalInput: 'do it', cwd: '/tmp' });
|
|
652
|
+
await stepStarted;
|
|
653
|
+
await waitForChildStarted(persistence, run.instanceId, 'step-prompt');
|
|
654
|
+
|
|
655
|
+
await engine.pauseAllSteps(run.instanceId);
|
|
656
|
+
await flushMicrotasks();
|
|
657
|
+
|
|
658
|
+
const state = persistence.state.get(run.instanceId);
|
|
659
|
+
expect(state?.executedSteps.find(s => s.stepId === 'step-prompt')?.status).toBe('paused');
|
|
660
|
+
expect(state?.pausedChildIds).toEqual(['child-1']);
|
|
661
|
+
expect(subagents.interrupts).toHaveLength(1);
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
describe('LifecycleManager registry', () => {
|
|
666
|
+
it('reconstructs the in-memory registry from snapshot pausedChildIds', () => {
|
|
667
|
+
const manager = new LifecycleManager({
|
|
668
|
+
hostAgent: new FakeAgent('host'),
|
|
669
|
+
initialPausedChildIds: ['child-1', 'child-2'],
|
|
670
|
+
interrupt: () => {},
|
|
671
|
+
followup: async () => {},
|
|
672
|
+
getAgent: () => undefined,
|
|
673
|
+
signal: new AbortController().signal,
|
|
674
|
+
readState: () => minimalState(),
|
|
675
|
+
commitState: () => {},
|
|
676
|
+
emitStepPaused: () => {},
|
|
677
|
+
emitStepResumed: () => {},
|
|
678
|
+
});
|
|
679
|
+
expect(manager.hasPaused('child-1')).toBe(true);
|
|
680
|
+
expect(manager.hasPaused('child-2')).toBe(true);
|
|
681
|
+
expect(manager.pausedIds).toEqual(['child-1', 'child-2']);
|
|
682
|
+
});
|
|
683
|
+
});
|