@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,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生命周期协调器(单步/全局暂停与恢复的纯协调器)。
|
|
3
|
+
*
|
|
4
|
+
* `LifecycleManager` 是暂停/恢复能力的「纯协调器」:不持有引擎状态,仅经注入的
|
|
5
|
+
* 回调/选项访问宿主主代理、`subagents.interrupt`/`followup`、`readState`/
|
|
6
|
+
* `commitState`(不可变重建 + 持久化)、`emitStepPaused`/`emitStepResumed`(事件
|
|
7
|
+
* 双发)与暂停登记表读写。暂停登记表(内存 `Set<string>`,值 = 子代理 SessionId)
|
|
8
|
+
* 是「暂停集」的唯一权威,与 `WorkflowTaskState.pausedChildIds` 数组同步(登记即
|
|
9
|
+
* add + 持久化,恢复即 delete + 持久化);`Agent.status`(仅 `idle|running`,无
|
|
10
|
+
* paused 态)不能替代登记表查询。
|
|
11
|
+
*
|
|
12
|
+
* 单步暂停:`executedSteps[].sessionMeta.childId` 反查子代理 SessionId →
|
|
13
|
+
* `interrupt(childId, { kind: 'ancestor', agent: hostAgent })` → 登记表 add →
|
|
14
|
+
* Step 置 `paused`(`pausedAt`)→ 持久化 → 双发 `sandore/step-paused`。已空闲
|
|
15
|
+
* Step(`agent.status === 'idle' && !inbox.hasPending`)幂等 no-op;不存在/终态
|
|
16
|
+
* Step 抛 `SandoreError('INVALID_OPERATION')`。单步恢复:反查 childId → 移出登记表
|
|
17
|
+
* → `followup(hostAgent, childId, text(resumePrompt ?? 默认提示))` → Step 置
|
|
18
|
+
* `running`(`resumedAt`/`resumePrompt`)→ 持久化 → 双发 `sandore/step-resumed`;
|
|
19
|
+
* 非登记 Step 抛 `INVALID_OPERATION`。
|
|
20
|
+
* @module dsh-sandore/workflow/src/lifecycle-manager
|
|
21
|
+
*/
|
|
22
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
23
|
+
|
|
24
|
+
import type { Agent, SubagentInterruptAuthority } from '../microflow/subagents';
|
|
25
|
+
import { SandoreError } from './errors.ts';
|
|
26
|
+
import type { WorkflowTaskState } from './schema.ts';
|
|
27
|
+
import type { UserMessageRecord } from './schema.ts';
|
|
28
|
+
import type { StepStatus } from './types.ts';
|
|
29
|
+
|
|
30
|
+
/** 已执行 Step 的运行时形状(与状态快照 `executedSteps` 元素严格一致)。 */
|
|
31
|
+
type ExecutedStep = WorkflowTaskState['executedSteps'][number];
|
|
32
|
+
|
|
33
|
+
/** Step 终态集合:暂停对终态 Step 抛 `INVALID_OPERATION`。 */
|
|
34
|
+
const TERMINAL_STEP_STATUSES: ReadonlySet<StepStatus> = new Set(['completed', 'failed']);
|
|
35
|
+
|
|
36
|
+
/** 恢复提示词的缺省值(`resumeStep`/`resumeAllSteps` 未显式提供时使用)。 */
|
|
37
|
+
const DEFAULT_RESUME_PROMPT = '请继续完成当前任务。';
|
|
38
|
+
|
|
39
|
+
/** 合成新子代理 SessionId(纯 UUID 格式)。 */
|
|
40
|
+
export function synthesizeChildId(_stepId: string): string {
|
|
41
|
+
return globalThis.crypto.randomUUID();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 当前 ISO 时间戳。 */
|
|
45
|
+
function now(): string {
|
|
46
|
+
return new Date().toISOString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 生命周期协调器的依赖(全部经回调/选项注入,保持纯协调器可单测)。 */
|
|
50
|
+
export interface LifecycleManagerOptions {
|
|
51
|
+
/** 宿主主代理(interrupt 的 ancestor authority + followup 的 parent 三合一)。 */
|
|
52
|
+
readonly hostAgent: Agent;
|
|
53
|
+
/** 暂停登记表初始值(startInstance 为空,崩溃恢复时从快照 `pausedChildIds` 重建)。 */
|
|
54
|
+
readonly initialPausedChildIds: readonly string[];
|
|
55
|
+
/** `ctx.subagents.interrupt` 的注入包装。 */
|
|
56
|
+
readonly interrupt: (childId: string, authority: SubagentInterruptAuthority) => void;
|
|
57
|
+
/** `ctx.subagents.followup` 的注入包装。 */
|
|
58
|
+
readonly followup: (childId: string, content: ContentBlock[], signal: AbortSignal) => Promise<void> | void;
|
|
59
|
+
/** 按 childId 查活子代理(空闲幂等 no-op 判定用)。 */
|
|
60
|
+
readonly getAgent: (childId: string) => Agent | undefined;
|
|
61
|
+
/** 引擎级取消信号(followup 的 `options.signal`)。 */
|
|
62
|
+
readonly signal: AbortSignal;
|
|
63
|
+
/** 读当前完整状态快照。 */
|
|
64
|
+
readonly readState: () => WorkflowTaskState;
|
|
65
|
+
/** 不可变重建 + 持久化当前状态快照。 */
|
|
66
|
+
readonly commitState: (next: WorkflowTaskState) => void;
|
|
67
|
+
/** 追加一条用户指令消息(写入 `state.userMessages` 并持久化;广播只记一条)。 */
|
|
68
|
+
readonly appendUserMessage: (record: UserMessageRecord) => void;
|
|
69
|
+
/** 双发 `sandore/step-paused` 事件(含 Step 名称与可选循环迭代路径)。 */
|
|
70
|
+
readonly emitStepPaused: (stepId: string, childId: string, name: string, loopIteration?: number[]) => void;
|
|
71
|
+
/** 双发 `sandore/step-resumed` 事件(含 Step 名称与可选循环迭代路径)。 */
|
|
72
|
+
readonly emitStepResumed: (stepId: string, childId: string, resumePrompt: string | undefined, name: string, loopIteration?: number[]) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 生命周期协调器。
|
|
77
|
+
*
|
|
78
|
+
* 持有暂停登记表(内存 `Set`),通过注入的依赖驱动单步/全局暂停与恢复;状态更新
|
|
79
|
+
* 以「不可变重建 + commitState」落到引擎 `active.state` 与持久化快照。
|
|
80
|
+
*/
|
|
81
|
+
export class LifecycleManager {
|
|
82
|
+
private readonly pausedChildIds: Set<string>;
|
|
83
|
+
|
|
84
|
+
constructor(private readonly deps: LifecycleManagerOptions) {
|
|
85
|
+
this.pausedChildIds = new Set(deps.initialPausedChildIds);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 暂停登记表当前内容(子代理 SessionId 集合,仅只读投影)。 */
|
|
89
|
+
get pausedIds(): readonly string[] {
|
|
90
|
+
return [...this.pausedChildIds];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 子代理自然完成(interrupt 软信号未生效,Step 已 completed)时清理暂停登记表:
|
|
95
|
+
* 移除该 childId 并同步持久化。pause→完成竞态(pauseStep 先 interrupt+登记,但子代理
|
|
96
|
+
* 仍跑完)后,若不清登记会残留「段 completed + 登记表有值」的不一致,resume 将卡死。
|
|
97
|
+
* @param childId - 已完成的子代理 SessionId。
|
|
98
|
+
*/
|
|
99
|
+
clearPausedOnCompletion(childId: string): void {
|
|
100
|
+
if (!this.pausedChildIds.has(childId)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.pausedChildIds.delete(childId);
|
|
104
|
+
this.commitPausedRegistry();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 某子代理 SessionId 是否在暂停登记表中。 */
|
|
108
|
+
hasPaused(childId: string): boolean {
|
|
109
|
+
return this.pausedChildIds.has(childId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 暂停单个 Step(经 childId 反查 + interrupt + 登记表 add + 状态 paused + 事件)。 */
|
|
113
|
+
pauseStep(stepId: string): void {
|
|
114
|
+
const step = this.findStep(stepId);
|
|
115
|
+
if (step === undefined) {
|
|
116
|
+
throw new SandoreError(`step '${stepId}' not found`, 'INVALID_OPERATION');
|
|
117
|
+
}
|
|
118
|
+
if (this.isNonSubagentCommand(step)) {
|
|
119
|
+
// shell Step(sessionMeta.synthetic 合成占位,无真实子代理)不可暂停:
|
|
120
|
+
// interrupt 一个无活代理 id 是静默 no-op,会误标 paused(假暂停)。直接拒绝。
|
|
121
|
+
throw new SandoreError(
|
|
122
|
+
`step '${stepId}' is a non-subagent step (${step.command?.type}) and cannot be paused`,
|
|
123
|
+
'INVALID_OPERATION',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (TERMINAL_STEP_STATUSES.has(step.status)) {
|
|
127
|
+
throw new SandoreError(`step '${stepId}' is in terminal state '${step.status}'`, 'INVALID_OPERATION');
|
|
128
|
+
}
|
|
129
|
+
const childId = this.childIdOf(step);
|
|
130
|
+
if (this.pausedChildIds.has(childId)) {
|
|
131
|
+
// 已在登记表:重复暂停幂等 no-op(避免重复 interrupt / 重复标记)。
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const child = this.deps.getAgent(childId);
|
|
135
|
+
if (child !== undefined && child.status === 'idle' && !child.inbox.hasPending) {
|
|
136
|
+
// 目标子代理恰好在 interrupt 前完成(空闲且 inbox 无 pending):幂等 no-op,
|
|
137
|
+
// 不产生中断副作用、不误标 paused(按 Step 实际完成状态处理)。
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.deps.interrupt(childId, { kind: 'ancestor', agent: this.deps.hostAgent });
|
|
141
|
+
this.pausedChildIds.add(childId);
|
|
142
|
+
this.commitStepStatus(step, 'paused', { pausedAt: now() });
|
|
143
|
+
this.deps.emitStepPaused(this.stepIdOf(step), childId, step.name, step.loopIteration);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 恢复单个 Step(反查 childId + 移出登记表 + followup + 追加新执行段 + 用户消息)。
|
|
148
|
+
*
|
|
149
|
+
* 段模型:被暂停段保持 paused 定格快照;恢复时以它为基底创建**新执行段**
|
|
150
|
+
* (新唯一执行 id + attemptIndex+1 + createdAt),followup 唤醒同一子代理续跑。
|
|
151
|
+
* 恢复消息不写入段(统一存 `userMessages`,避免广播时每段重复同一句话)。
|
|
152
|
+
*/
|
|
153
|
+
async resumeStep(stepId: string, resumePrompt?: string): Promise<void> {
|
|
154
|
+
const step = this.findStep(stepId);
|
|
155
|
+
if (step === undefined) {
|
|
156
|
+
throw new SandoreError(`step '${stepId}' not found`, 'INVALID_OPERATION');
|
|
157
|
+
}
|
|
158
|
+
const childId = this.childIdOf(step);
|
|
159
|
+
if (!this.pausedChildIds.has(childId)) {
|
|
160
|
+
throw new SandoreError(`step '${stepId}' is not paused`, 'INVALID_OPERATION');
|
|
161
|
+
}
|
|
162
|
+
const content: ContentBlock[] = [{ type: 'text', text: resumePrompt ?? DEFAULT_RESUME_PROMPT }];
|
|
163
|
+
await this.deps.followup(childId, content, this.deps.signal);
|
|
164
|
+
this.pausedChildIds.delete(childId);
|
|
165
|
+
// 段创建权归执行器(stage-executor resumePendingPaused 基于旧段新建段对象);
|
|
166
|
+
// lifecycle 只负责 followup + 登记表 + 用户消息 + 事件。段内不写 resumePrompt。
|
|
167
|
+
this.deps.appendUserMessage(this.userMessage('resume', this.stepIdOf(step), resumePrompt ?? DEFAULT_RESUME_PROMPT));
|
|
168
|
+
this.commitPausedRegistry();
|
|
169
|
+
this.deps.emitStepResumed(this.stepIdOf(step), childId, resumePrompt, step.name, step.loopIteration);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 暂停实例内全部 running/interacting Step(completed/failed 与非子代理 Step 跳过)。 */
|
|
173
|
+
pauseAllSteps(): void {
|
|
174
|
+
const targets = this.deps.readState().executedSteps.filter(
|
|
175
|
+
step =>
|
|
176
|
+
(step.status === 'running' || step.status === 'interacting') &&
|
|
177
|
+
!this.isNonSubagentCommand(step),
|
|
178
|
+
);
|
|
179
|
+
for (const step of targets) {
|
|
180
|
+
this.pauseStep(this.stepIdOf(step));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 恢复登记表内全部 Step(逐个 followup + 移表,完成后登记表清空)。
|
|
186
|
+
* 广播指令是实例级事件:只追加一条 `target:'broadcast'` 用户消息(不得复制进每个 Step)。
|
|
187
|
+
*/
|
|
188
|
+
async resumeAllSteps(resumePrompt?: string): Promise<void> {
|
|
189
|
+
const childIds = [...this.pausedChildIds];
|
|
190
|
+
for (const childId of childIds) {
|
|
191
|
+
const step = this.findStepByChildId(childId);
|
|
192
|
+
if (step === undefined) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const content: ContentBlock[] = [{ type: 'text', text: resumePrompt ?? DEFAULT_RESUME_PROMPT }];
|
|
196
|
+
await this.deps.followup(childId, content, this.deps.signal);
|
|
197
|
+
this.pausedChildIds.delete(childId);
|
|
198
|
+
// 段创建由执行器续跑完成(resumePendingPaused);此处只发事件(含 Step 名称)。
|
|
199
|
+
this.deps.emitStepResumed(this.stepIdOf(step), childId, resumePrompt, step.name, step.loopIteration);
|
|
200
|
+
}
|
|
201
|
+
if (childIds.length > 0) {
|
|
202
|
+
this.deps.appendUserMessage(this.userMessage('resume', 'broadcast', resumePrompt ?? DEFAULT_RESUME_PROMPT));
|
|
203
|
+
this.commitPausedRegistry();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 登记表变更后同步持久化(pausedChildIds + 实例 status;空表 → running)。 */
|
|
208
|
+
private commitPausedRegistry(): void {
|
|
209
|
+
const state = this.deps.readState();
|
|
210
|
+
const next: WorkflowTaskState = {
|
|
211
|
+
...state,
|
|
212
|
+
status: this.pausedChildIds.size > 0 ? 'paused' : 'running',
|
|
213
|
+
pausedChildIds: [...this.pausedChildIds],
|
|
214
|
+
updatedAt: now(),
|
|
215
|
+
};
|
|
216
|
+
this.deps.commitState(next);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 按 stepId(定义 id 或唯一执行 id)反查"最后活跃段"(status running/interacting 中
|
|
221
|
+
* attemptIndex 最大者);无活跃段时回退同 stepId 的任意段(再退唯一执行 id)。
|
|
222
|
+
* 段模型下同 step 可有多段(attempt0 paused + attempt1 running),暂停/恢复必须定位活跃段。
|
|
223
|
+
*/
|
|
224
|
+
private findStep(stepId: string): ExecutedStep | undefined {
|
|
225
|
+
const steps = this.deps.readState().executedSteps;
|
|
226
|
+
const byStep = steps.filter(step => step.stepId === stepId);
|
|
227
|
+
if (byStep.length > 0) {
|
|
228
|
+
const active = byStep
|
|
229
|
+
.filter(step => step.status === 'running' || step.status === 'interacting')
|
|
230
|
+
.sort((a, b) => (b.attemptIndex ?? 0) - (a.attemptIndex ?? 0));
|
|
231
|
+
return active[0] ?? byStep[byStep.length - 1];
|
|
232
|
+
}
|
|
233
|
+
return steps.find(step => step.id === stepId);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** 按子代理 SessionId 反查 Step(`resumeAllSteps` 遍历登记表用)。 */
|
|
237
|
+
private findStepByChildId(childId: string): ExecutedStep | undefined {
|
|
238
|
+
const steps = this.deps.readState().executedSteps;
|
|
239
|
+
return steps.find(step => this.childIdOf(step) === childId);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** 反查 Step 的子代理 SessionId(缺省回退唯一执行 id)。 */
|
|
243
|
+
private childIdOf(step: ExecutedStep): string {
|
|
244
|
+
return step.sessionMeta?.childId ?? step.id;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** 判定 Step 是否为非子代理命令(shell 合成占位,无真实子代理,不可暂停)。 */
|
|
248
|
+
private isNonSubagentCommand(step: ExecutedStep): boolean {
|
|
249
|
+
const type = step.command?.type;
|
|
250
|
+
return type === 'shell';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Step 对外展示 id(定义 stepId,缺省回退唯一执行 id)。 */
|
|
254
|
+
private stepIdOf(step: ExecutedStep): string {
|
|
255
|
+
return step.stepId ?? step.id;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 不可变重建状态:更新单个 Step 状态 + 同步登记表 + 持久化。 */
|
|
259
|
+
private commitStepStatus(
|
|
260
|
+
step: ExecutedStep,
|
|
261
|
+
status: StepStatus,
|
|
262
|
+
extra: { pausedAt?: string },
|
|
263
|
+
): void {
|
|
264
|
+
const state = this.deps.readState();
|
|
265
|
+
const next: WorkflowTaskState = {
|
|
266
|
+
...state,
|
|
267
|
+
status: this.pausedChildIds.size > 0 ? 'paused' : 'running',
|
|
268
|
+
executedSteps: state.executedSteps.map(item => {
|
|
269
|
+
if (item.id !== step.id) {
|
|
270
|
+
return item;
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
...item,
|
|
274
|
+
status,
|
|
275
|
+
...(extra.pausedAt === undefined ? {} : { pausedAt: extra.pausedAt }),
|
|
276
|
+
};
|
|
277
|
+
}),
|
|
278
|
+
pausedChildIds: [...this.pausedChildIds],
|
|
279
|
+
updatedAt: now(),
|
|
280
|
+
};
|
|
281
|
+
this.deps.commitState(next);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 构造一条用户消息记录(统一时间轴)。 */
|
|
285
|
+
private userMessage(kind: 'input' | 'resume' | 'retry' | 'answer', target: string, content: string): UserMessageRecord {
|
|
286
|
+
return { id: `usr-${globalThis.crypto.randomUUID()}`, kind, target, content, createdAt: now() };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* persistence.ts(SandorePersistence)的单元测试:域打开/关闭生命周期与
|
|
3
|
+
* 状态快照全量覆盖写。
|
|
4
|
+
* @module dsh-sandore/workflow/src/persistence.spec
|
|
5
|
+
*/
|
|
6
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
7
|
+
import type { DomainFacility } from '@deepseek-ai/dsh-storage-domain'
|
|
8
|
+
import { describe, expect, it } from 'vitest'
|
|
9
|
+
|
|
10
|
+
import { MemoryStorageDomain } from '../tests/mocks/storage-domain.ts'
|
|
11
|
+
import { SandorePersistence } from './persistence.ts'
|
|
12
|
+
import type { WorkflowTaskState } from './schema.ts'
|
|
13
|
+
|
|
14
|
+
/** 构造一个字段完整的 `WorkflowTaskState` 快照(`taskId` 必填)。 */
|
|
15
|
+
function makeState(overrides: Partial<WorkflowTaskState> & { taskId: string }): WorkflowTaskState {
|
|
16
|
+
const { taskId, ...rest } = overrides
|
|
17
|
+
const now = new Date().toISOString()
|
|
18
|
+
return {
|
|
19
|
+
taskId,
|
|
20
|
+
jobId: 'job-1',
|
|
21
|
+
workflowId: 'wfl-1',
|
|
22
|
+
cwd: '/tmp',
|
|
23
|
+
status: 'running',
|
|
24
|
+
originalInput: 'do the thing',
|
|
25
|
+
currentStageId: '',
|
|
26
|
+
executedSteps: [],
|
|
27
|
+
stageInfos: [],
|
|
28
|
+
pendingInteractions: [],
|
|
29
|
+
pendingQueue: [],
|
|
30
|
+
visitCounts: {},
|
|
31
|
+
passThroughVars: {},
|
|
32
|
+
activeLoopIterationStack: [],
|
|
33
|
+
activeLoopNodeId: null,
|
|
34
|
+
activeLoopStack: [],
|
|
35
|
+
pausedChildIds: [],
|
|
36
|
+
stageContracts: {},
|
|
37
|
+
createdAt: now,
|
|
38
|
+
updatedAt: now,
|
|
39
|
+
...rest,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 组装测试 harness:注入内存 storageDomain 并加载 SandorePersistence。 */
|
|
44
|
+
async function harness(): Promise<{ ctx: Context; storageDomain: MemoryStorageDomain }> {
|
|
45
|
+
const ctx = new Context()
|
|
46
|
+
const storageDomain = new MemoryStorageDomain()
|
|
47
|
+
ctx.provide('storageDomain', storageDomain as unknown as DomainFacility)
|
|
48
|
+
await ctx.plugin(SandorePersistence)
|
|
49
|
+
return { ctx, storageDomain }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('SandorePersistence', () => {
|
|
53
|
+
it('opens the sandore domain and resolves the three table handles on init', async () => {
|
|
54
|
+
const { ctx, storageDomain } = await harness()
|
|
55
|
+
const persistence = ctx.sandorePersistence
|
|
56
|
+
|
|
57
|
+
expect(storageDomain.get('sandore')).toBeDefined()
|
|
58
|
+
expect(storageDomain.get('sandore')?.name).toBe('sandore')
|
|
59
|
+
expect(persistence.definitions.size).toBe(0)
|
|
60
|
+
expect(persistence.state.size).toBe(0)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('closes the domain when the provider fiber unloads', async () => {
|
|
64
|
+
const ctx = new Context()
|
|
65
|
+
const storageDomain = new MemoryStorageDomain()
|
|
66
|
+
ctx.provide('storageDomain', storageDomain as unknown as DomainFacility)
|
|
67
|
+
const fiber = ctx.plugin(SandorePersistence)
|
|
68
|
+
await fiber
|
|
69
|
+
|
|
70
|
+
expect(storageDomain.get('sandore')?.closed).toBe(false)
|
|
71
|
+
await fiber.dispose()
|
|
72
|
+
expect(storageDomain.get('sandore')?.closed).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('overwrites the full snapshot for the same instanceId on repeated saveWorkflowState', async () => {
|
|
76
|
+
const { ctx } = await harness()
|
|
77
|
+
const persistence = ctx.sandorePersistence
|
|
78
|
+
|
|
79
|
+
const first = makeState({
|
|
80
|
+
taskId: 'inst-1',
|
|
81
|
+
originalInput: 'first input',
|
|
82
|
+
executedSteps: [
|
|
83
|
+
{
|
|
84
|
+
type: 'default',
|
|
85
|
+
id: 'step-1',
|
|
86
|
+
stepId: 'step-1',
|
|
87
|
+
stageId: 'stg-1',
|
|
88
|
+
name: 'lint',
|
|
89
|
+
status: 'completed',
|
|
90
|
+
visitCount: 1,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
})
|
|
94
|
+
const second = makeState({
|
|
95
|
+
taskId: 'inst-1',
|
|
96
|
+
originalInput: 'second input',
|
|
97
|
+
status: 'completed',
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await persistence.saveWorkflowState(first)
|
|
101
|
+
await persistence.saveWorkflowState(second)
|
|
102
|
+
|
|
103
|
+
const snapshot = persistence.state.get('inst-1')
|
|
104
|
+
expect(snapshot).toEqual(second)
|
|
105
|
+
expect(snapshot?.originalInput).toBe('second input')
|
|
106
|
+
// 全量覆盖:旧快照的 executedSteps 不应残留。
|
|
107
|
+
expect(snapshot?.executedSteps).toHaveLength(0)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sandore 存储域持久化 Provider。
|
|
3
|
+
*
|
|
4
|
+
* `SandorePersistence` 在 `[Service.init]` 中打开 `sandore` 域(两表
|
|
5
|
+
* `definitions`/`state`,去 job 概念),用 `ctx.effect` 挂 `Domain.close()` 释放,
|
|
6
|
+
* 并暴露两表 `KvTable` 句柄与 `saveWorkflowState`(`state.put(taskId, state)`
|
|
7
|
+
* 全量覆盖)。`SandoreRemote` 通过 `ctx.sandorePersistence` 访问这些句柄。
|
|
8
|
+
* @module dsh-sandore/workflow/src/persistence
|
|
9
|
+
*/
|
|
10
|
+
import { Context, Service } from '@deepseek-ai/cordis'
|
|
11
|
+
import type { KvTable } from '@deepseek-ai/dsh-storage-domain'
|
|
12
|
+
|
|
13
|
+
import { sandoreDomainSpec } from './domain.ts'
|
|
14
|
+
import type { WorkflowDefinitionRecord, WorkflowTaskState } from './schema.ts'
|
|
15
|
+
|
|
16
|
+
declare module '@deepseek-ai/cordis' {
|
|
17
|
+
interface Context {
|
|
18
|
+
sandorePersistence: SandorePersistence
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** sandore 存储域持久化 Provider。 */
|
|
23
|
+
export class SandorePersistence extends Service {
|
|
24
|
+
static inject = ['storageDomain']
|
|
25
|
+
|
|
26
|
+
private definitionsTable?: KvTable<string, WorkflowDefinitionRecord>
|
|
27
|
+
private stateTable?: KvTable<string, WorkflowTaskState>
|
|
28
|
+
|
|
29
|
+
constructor(ctx: Context) {
|
|
30
|
+
super(ctx, 'sandorePersistence')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected async [Service.init](): Promise<void> {
|
|
34
|
+
const domain = await this.ctx.storageDomain.open(sandoreDomainSpec)
|
|
35
|
+
this.ctx.effect(() => () => domain.close(), 'sandore.domainClose')
|
|
36
|
+
this.definitionsTable = domain.table('definitions')
|
|
37
|
+
this.stateTable = domain.table('state')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `definitions` 表句柄(key = workflowId)。 */
|
|
41
|
+
get definitions(): KvTable<string, WorkflowDefinitionRecord> {
|
|
42
|
+
return this.requireTable(this.definitionsTable, 'definitions')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `state` 表句柄(key = instanceId)。 */
|
|
46
|
+
get state(): KvTable<string, WorkflowTaskState> {
|
|
47
|
+
return this.requireTable(this.stateTable, 'state')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 全量覆盖写入实例状态快照。
|
|
52
|
+
* @param state - 完整运行时状态快照;以 `state.taskId`(instanceId)为键。
|
|
53
|
+
*/
|
|
54
|
+
async saveWorkflowState(state: WorkflowTaskState): Promise<void> {
|
|
55
|
+
await this.state.put(state.taskId, state)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private requireTable<V>(table: KvTable<string, V> | undefined, name: string): KvTable<string, V> {
|
|
59
|
+
if (table === undefined) {
|
|
60
|
+
throw new Error(`sandore persistence is not initialized ('${name}' table unavailable)`)
|
|
61
|
+
}
|
|
62
|
+
return table
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sandore workflow 包的复合插件(默认导出)。
|
|
3
|
+
*
|
|
4
|
+
* `workflow` 包由三个独立 Service 组成一条依赖链:
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* storageDomain ──▶ SandorePersistence (sandorePersistence)
|
|
8
|
+
* sandorePersistence + sessions + … ──▶ SandoreEngineRuntime (sandoreEngine)
|
|
9
|
+
* sandorePersistence + sandoreEngine + llm ──▶ SandoreRemote (sandore RPC)
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* 单元测试里三者经 `ctx.plugin(...)` 逐个注册,但生产 cordis.yml 只加载包默认
|
|
13
|
+
* 导出。因此本复合插件在 `[Service.init]` 中按拓扑序依次 `ctx.plugin(...)` 注册
|
|
14
|
+
* 三个子服务,使 `sandorePersistence` / `sandoreEngine` / `sandore` RPC 全部对
|
|
15
|
+
* 宿主可用;自身不 `inject` 任何子服务(它们各自声明依赖),Config 透传
|
|
16
|
+
* `SandoreEngineRuntime.Config`。
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-sandore/workflow/plugin
|
|
19
|
+
*/
|
|
20
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
21
|
+
|
|
22
|
+
import { SandorePersistence } from './persistence.ts';
|
|
23
|
+
import { SandoreRemote } from './remote.ts';
|
|
24
|
+
import type { SandoreConfig } from './seam.ts';
|
|
25
|
+
import SandoreEngineRuntime from './workflow-engine.ts';
|
|
26
|
+
|
|
27
|
+
export default class SandoreWorkflowPlugin extends Service {
|
|
28
|
+
static Config = SandoreEngineRuntime.Config;
|
|
29
|
+
|
|
30
|
+
constructor(ctx: Context, private readonly config: SandoreConfig) {
|
|
31
|
+
super(ctx, 'sandoreWorkflow');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected async [Service.init](): Promise<void> {
|
|
35
|
+
await this.ctx.plugin(SandorePersistence);
|
|
36
|
+
await this.ctx.plugin(SandoreEngineRuntime, this.config);
|
|
37
|
+
await this.ctx.plugin(SandoreRemote);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ReactiveState } from './reactive-state.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ReactiveState 新语义(纯显式 flush,无自动调度):
|
|
6
|
+
* - set/update 只更新内存最新值,不自动 persist(避免微任务并发乱序)
|
|
7
|
+
* - flush() 立即持久化当前最新值并等待完成(关键路径)
|
|
8
|
+
*/
|
|
9
|
+
describe('ReactiveState', () => {
|
|
10
|
+
it('get current 返回初始值', () => {
|
|
11
|
+
const rs = new ReactiveState({ a: 1 }, async () => {});
|
|
12
|
+
expect(rs.current).toEqual({ a: 1 });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('set current 立即更新 current 但不自动 persist', () => {
|
|
16
|
+
const persist = vi.fn(async () => {});
|
|
17
|
+
const rs = new ReactiveState({ a: 1 }, persist);
|
|
18
|
+
rs.current = { a: 2 };
|
|
19
|
+
expect(rs.current).toEqual({ a: 2 });
|
|
20
|
+
expect(persist).not.toHaveBeenCalled();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('flush 立即持久化当前值并等待完成', async () => {
|
|
24
|
+
const persist = vi.fn(async () => {});
|
|
25
|
+
const rs = new ReactiveState({ a: 1 }, persist);
|
|
26
|
+
rs.current = { a: 2 };
|
|
27
|
+
await rs.flush();
|
|
28
|
+
expect(persist).toHaveBeenCalledTimes(1);
|
|
29
|
+
expect(persist).toHaveBeenCalledWith({ a: 2 });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('flush 在赋值后落盘最新值(多次赋值只写最后一次 flush 的值)', async () => {
|
|
33
|
+
const persist = vi.fn(async () => {});
|
|
34
|
+
const rs = new ReactiveState({ a: 1 }, persist);
|
|
35
|
+
rs.current = { a: 2 };
|
|
36
|
+
rs.current = { a: 3 };
|
|
37
|
+
rs.current = { a: 4 };
|
|
38
|
+
// 三次赋值都不自动 persist;flush 写最新 { a: 4 }
|
|
39
|
+
expect(persist).not.toHaveBeenCalled();
|
|
40
|
+
await rs.flush();
|
|
41
|
+
expect(persist).toHaveBeenCalledTimes(1);
|
|
42
|
+
expect(persist).toHaveBeenCalledWith({ a: 4 });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('update 函数式更新内存值(不自动 persist)', () => {
|
|
46
|
+
const persist = vi.fn(async () => {});
|
|
47
|
+
const rs = new ReactiveState({ count: 0 }, persist);
|
|
48
|
+
rs.update(current => ({ count: current.count + 1 }));
|
|
49
|
+
expect(rs.current).toEqual({ count: 1 });
|
|
50
|
+
expect(persist).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('onError 兜底捕获 flush 的 persist 异常', async () => {
|
|
54
|
+
const error = new Error('persist failed');
|
|
55
|
+
const onError = vi.fn();
|
|
56
|
+
const rs = new ReactiveState({ a: 1 }, async () => { throw error; }, onError);
|
|
57
|
+
await rs.flush();
|
|
58
|
+
expect(onError).toHaveBeenCalledWith(error);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('forcePersist 等价 flush(立即持久化当前值)', async () => {
|
|
62
|
+
const persist = vi.fn(async () => {});
|
|
63
|
+
const rs = new ReactiveState({ a: 1 }, persist);
|
|
64
|
+
rs.current = { a: 2 };
|
|
65
|
+
rs.forcePersist();
|
|
66
|
+
await rs.flush();
|
|
67
|
+
expect(persist).toHaveBeenCalledWith({ a: 2 });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 响应式状态包装器。
|
|
3
|
+
*
|
|
4
|
+
* 只负责「安全地持有最新值」+「显式 flush 落盘」。不做自动调度——
|
|
5
|
+
* 持久化由业务代码的 `persistState`(→ flush)显式触发,避免微任务并发乱序。
|
|
6
|
+
* 这样 `active.state = {...}` 赋值后,紧随的 `await persistState` 同步写当前值,
|
|
7
|
+
* 与改造前的「显式 persistState 立即写库」语义完全一致。
|
|
8
|
+
*/
|
|
9
|
+
export class ReactiveState<T extends object> {
|
|
10
|
+
private latest: T;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
initial: T,
|
|
14
|
+
private readonly persist: (state: T) => Promise<void>,
|
|
15
|
+
private readonly onError?: (error: unknown) => void,
|
|
16
|
+
) {
|
|
17
|
+
this.latest = initial;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 读取当前状态(同步,零开销)。 */
|
|
21
|
+
get current(): T {
|
|
22
|
+
return this.latest;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 替换状态(仅更新内存最新值,不自动持久化——由显式 flush 落盘)。 */
|
|
26
|
+
set current(next: T) {
|
|
27
|
+
this.latest = next;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 批量更新(函数式,避免中间态暴露)。 */
|
|
31
|
+
update(fn: (current: T) => T): void {
|
|
32
|
+
this.latest = fn(this.latest);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 立即持久化当前值并等待完成(关键路径)。
|
|
37
|
+
* 同步捕获 latest → persist → await 完成,返回即落盘。
|
|
38
|
+
*/
|
|
39
|
+
async flush(): Promise<void> {
|
|
40
|
+
const snapshot = this.latest;
|
|
41
|
+
try {
|
|
42
|
+
await this.persist(snapshot);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
this.onError?.(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 兼容旧接口:等价 flush(无自动调度语义)。 */
|
|
49
|
+
forcePersist(): void {
|
|
50
|
+
void this.flush();
|
|
51
|
+
}
|
|
52
|
+
}
|