@co0ontty/wand 4.94.0 → 4.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/ai-text-runner.d.ts +4 -0
- package/dist/ai-text-runner.js +44 -4
- package/dist/build-info.json +3 -3
- package/dist/cli-api.d.ts +1 -0
- package/dist/cli-api.js +7 -2
- package/dist/cli.js +47 -0
- package/dist/conversation-execution-preference.d.ts +7 -0
- package/dist/conversation-execution-preference.js +26 -0
- package/dist/conversation-organize-contract.d.ts +70 -0
- package/dist/conversation-organize-contract.js +1 -0
- package/dist/conversation-organize-service.d.ts +46 -0
- package/dist/conversation-organize-service.js +505 -0
- package/dist/conversation-organize.d.ts +41 -0
- package/dist/conversation-organize.js +97 -0
- package/dist/conversation-service.d.ts +2 -1
- package/dist/conversation-service.js +10 -4
- package/dist/core-turn-tracker.d.ts +5 -1
- package/dist/core-turn-tracker.js +17 -1
- package/dist/decision-expert-service.d.ts +2 -0
- package/dist/decision-expert-service.js +17 -1
- package/dist/decision-skill.d.ts +0 -1
- package/dist/decision-skill.js +5 -64
- package/dist/deployment-lock.js +4 -1
- package/dist/employee-im-config.d.ts +17 -0
- package/dist/employee-im-config.js +85 -0
- package/dist/employee-im-lifecycle.d.ts +35 -0
- package/dist/employee-im-lifecycle.js +168 -0
- package/dist/employee-im-types.d.ts +80 -0
- package/dist/employee-im-types.js +32 -0
- package/dist/employee-im.d.ts +35 -0
- package/dist/employee-im.js +157 -0
- package/dist/employee-knowledge.js +3 -2
- package/dist/employee-text.d.ts +1 -0
- package/dist/employee-text.js +28 -13
- package/dist/git-quick-commit.js +21 -3
- package/dist/managed-skill.d.ts +6 -0
- package/dist/managed-skill.js +99 -0
- package/dist/model-auto-assign.js +1 -1
- package/dist/process-manager.js +1 -1
- package/dist/prompt-optimizer.js +1 -1
- package/dist/scheduled-task-activity.d.ts +16 -0
- package/dist/scheduled-task-activity.js +42 -0
- package/dist/scheduled-task-agenda.d.ts +20 -0
- package/dist/scheduled-task-agenda.js +82 -0
- package/dist/scheduled-task-calendar.d.ts +37 -0
- package/dist/scheduled-task-calendar.js +131 -0
- package/dist/scheduled-task-read-range.d.ts +7 -0
- package/dist/scheduled-task-read-range.js +35 -0
- package/dist/scheduled-task-service.d.ts +6 -1
- package/dist/scheduled-task-service.js +61 -1
- package/dist/scheduled-task-state.d.ts +1 -1
- package/dist/scheduled-task-types.d.ts +38 -0
- package/dist/server-asset-routes.js +3 -2
- package/dist/server-conversation-organize-routes.d.ts +4 -0
- package/dist/server-conversation-organize-routes.js +75 -0
- package/dist/server-conversation-routes.js +2 -1
- package/dist/server-employee-im-routes.d.ts +9 -0
- package/dist/server-employee-im-routes.js +40 -0
- package/dist/server-employee-routes.js +1 -1
- package/dist/server-scheduled-task-routes.js +24 -0
- package/dist/server-session-lifecycle-routes.js +8 -3
- package/dist/server-session-routes.js +18 -0
- package/dist/server-session-worktree-routes.js +3 -3
- package/dist/server-workspace-routes.d.ts +3 -0
- package/dist/server-workspace-routes.js +14 -1
- package/dist/server.js +39 -5
- package/dist/session-ai-context.d.ts +11 -1
- package/dist/session-ai-context.js +21 -2
- package/dist/session-metadata-service.js +1 -1
- package/dist/session-registry.js +6 -0
- package/dist/session-request.js +3 -0
- package/dist/session-topic.js +1 -1
- package/dist/silicon-employee-draft.js +1 -1
- package/dist/speech-polisher-service.d.ts +2 -0
- package/dist/speech-polisher-service.js +28 -6
- package/dist/storage/conversation-organization-repository.d.ts +13 -0
- package/dist/storage/conversation-organization-repository.js +33 -0
- package/dist/storage/migrations.js +10 -0
- package/dist/storage/secrets-repository.d.ts +7 -0
- package/dist/storage/secrets-repository.js +28 -1
- package/dist/storage/tasks-repository.js +4 -0
- package/dist/storage-schedules.d.ts +8 -1
- package/dist/storage-schedules.js +24 -0
- package/dist/storage-schema.js +9 -0
- package/dist/storage.d.ts +6 -1
- package/dist/storage.js +13 -0
- package/dist/structured-session-manager.d.ts +1 -0
- package/dist/structured-session-manager.js +18 -4
- package/dist/system-ai-call.d.ts +49 -0
- package/dist/system-ai-call.js +28 -0
- package/dist/system-ai-session-recorder.d.ts +30 -0
- package/dist/system-ai-session-recorder.js +239 -0
- package/dist/system-cli.d.ts +6 -0
- package/dist/system-cli.js +93 -0
- package/dist/system-skill.d.ts +5 -0
- package/dist/system-skill.js +19 -0
- package/dist/task-organize-contract.js +2 -2
- package/dist/task-title-organize-service.d.ts +1 -1
- package/dist/task-title-organize-service.js +9 -5
- package/dist/task-title-organize.js +13 -6
- package/dist/task-title-service.js +1 -1
- package/dist/task-title.js +1 -1
- package/dist/user-memory.js +1 -1
- package/dist/wand-task-sync.js +2 -1
- package/dist/web-ui/content/ai-teams.js +12 -12
- package/dist/web-ui/content/module-manifest.json +1 -1
- package/dist/web-ui/content/modules/{0051bd3d55f10ab7/library-bridge-DTMQIHVJ.js → 09842858f9d34a4e/library-bridge.js} +2 -2
- package/dist/web-ui/content/modules/1021ae40c5761e3f/sha256.js +1 -0
- package/dist/web-ui/content/modules/134f6120c0acaf57/host.js +1 -0
- package/dist/web-ui/content/modules/1d518ae6ed66fe5a/chunk.js +3 -0
- package/dist/web-ui/content/modules/1f93b5bd70381bb6/runtime.js +539 -0
- package/dist/web-ui/content/modules/26c68d4decdd18b4/markdown.js +1 -0
- package/dist/web-ui/content/modules/2ed8bafd5b4fecc5/workspace-window.js +1 -0
- package/dist/web-ui/content/modules/31ffc2f05f87652c/host.js +1 -0
- package/dist/web-ui/content/modules/35aaa85b9d0937af/chunk.js +20 -0
- package/dist/web-ui/content/modules/3664d0620f124292/chunk.js +3 -0
- package/dist/web-ui/content/modules/3cea480770cb184c/chunk.js +1 -0
- package/dist/web-ui/content/modules/3d12adf54647c1e2/host-bridge.js +1 -0
- package/dist/web-ui/content/modules/459cca2482a9a4ba/host.js +18 -0
- package/dist/web-ui/content/modules/474c9647130ef3e9/task-board-host.js +2 -0
- package/dist/web-ui/content/modules/4f74d2517f29134c/chunk.js +15 -0
- package/dist/web-ui/content/modules/52a218d74002a011/host.js +30 -0
- package/dist/web-ui/content/modules/54f2b10659ef8475/employee-card.js +1 -0
- package/dist/web-ui/content/modules/703fb1c670f7b199/chunk.js +1 -0
- package/dist/web-ui/content/modules/75511da23f051718/host.js +1 -0
- package/dist/web-ui/content/modules/{0051bd3d55f10ab7/host-2IVIZ2YC.js → 799b19da948bec8d/host.js} +3 -2
- package/dist/web-ui/content/modules/{0051bd3d55f10ab7/host-XNIR3NTJ.js → 7ef73b6c20d2504f/host.js} +3 -2
- package/dist/web-ui/content/modules/8f8f7a0a8874d29a/host.js +1 -0
- package/dist/web-ui/content/modules/90f2cd73cdf3a2cd/chat-render.js +25 -0
- package/dist/web-ui/content/modules/9510efce6caaa9ec/chunk.js +1 -0
- package/dist/web-ui/content/modules/9a0e65188739957f/chunk.js +67 -0
- package/dist/web-ui/content/modules/9eae1033f47e4526/host.js +1 -0
- package/dist/web-ui/content/modules/a2c5b246e57fa165/panel.js +1 -0
- package/dist/web-ui/content/modules/a50ce28f45bfeb2e/host.js +1 -0
- package/dist/web-ui/content/modules/adfc53789d5bedb2/chunk.js +1 -0
- package/dist/web-ui/content/modules/bcecb0cc14b7d2bd/chunk.js +1 -0
- package/dist/web-ui/content/modules/c4706f47c23f33e4/host.js +1 -0
- package/dist/web-ui/content/modules/{0051bd3d55f10ab7/host-Q524TMSR.js → c7de873095c061ab/host.js} +4 -3
- package/dist/web-ui/content/modules/d789f84a3f8765ae/page.js +6 -0
- package/dist/web-ui/content/modules/db4e2dbc7aed6353/chunk.js +1 -0
- package/dist/web-ui/content/modules/e4fd9906521b4aa3/chunk.js +1 -0
- package/dist/web-ui/content/modules/eb95c3ee0d42827a/chunk.js +6 -0
- package/dist/web-ui/content/modules/{0051bd3d55f10ab7/host-PHHNGVNL.js → ec68d93a95c1d5d6/host.js} +2 -2
- package/dist/web-ui/content/modules/f33ed89d1ccd3a2d/chunk.js +1 -0
- package/dist/web-ui/content/modules/f6502dab121f9d91/chunk.js +1 -0
- package/dist/web-ui/content/modules/fac83e61c0a8c8a7/host.js +1 -0
- package/dist/web-ui/content/scripts.js +1 -1
- package/dist/web-ui/embedded-assets.d.ts +200 -27
- package/dist/web-ui/embedded-assets.js +5 -4
- package/dist/web-ui/react/workspaces/sidebar-manage.d.ts +5 -0
- package/dist/web-ui/react/workspaces/sidebar-manage.js +27 -3
- package/dist/web-ui/running-activity.d.ts +22 -9
- package/dist/web-ui/running-activity.js +58 -42
- package/dist/web-ui/running-current-action.d.ts +12 -0
- package/dist/web-ui/running-current-action.js +74 -0
- package/dist/web-ui/scripts.d.ts +4 -0
- package/dist/web-ui/scripts.js +17 -8
- package/package.json +4 -2
- package/scripts/recover-web-update.mjs +26 -19
- package/scripts/recover-web-update.sh +9 -6
- package/scripts/update-node.sh +3 -1
- package/skills/wand-system/SKILL.md +69 -0
- package/skills/wand-system/references/operations.md +100 -0
- package/skills/wand-system/scripts/wand.mjs +34 -0
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chat-render-RY2I2CF3.js +0 -24
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-2F6J2MKX.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-6C6A2RHG.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-7AFM245W.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-ECWYA3U5.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-MHX2QXIS.js +0 -111
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-RAAPETNH.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-RXKBHYUR.js +0 -38
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-T35D57FW.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-U4EDP4TI.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-WHXQR7JT.js +0 -20
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-YOXXNXOX.js +0 -2
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-YQJ6XYV7.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/employee-card-NV34TZIH.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-2AQAIUNO.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-AZXNFCRW.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-EPEQX22R.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-FZYIMOLC.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-G6GL4SNL.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-PEILC5NU.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-RO5MB6RB.js +0 -69
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-WOQRK6NG.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-XRVOIEP6.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-ZDKOQEMS.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-bridge-PMQZRG5G.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/page-PY4JUL4W.js +0 -3
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/panel-BBQFC55Q.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/runtime.js +0 -974
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/sha256-ANXV72UC.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/task-board-host-PXZSWBBG.js +0 -2
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/workspace-agent-picker-DJSECSY6.js +0 -1
- package/dist/web-ui/content/modules/0051bd3d55f10ab7/workspace-window-MNMKSOBG.js +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AiTeamRun } from "./ai-team-types.js";
|
|
2
2
|
import { type AiTeamRunner } from "./ai-team-runner.js";
|
|
3
3
|
import { type ConversationDetail, type ConversationInstance, type ConversationReceipt, type ConversationSummary, type ConversationTarget, type ConversationSessionUpdate } from "./conversation-types.js";
|
|
4
|
+
import { type ConversationExecutionPreference } from "./conversation-execution-preference.js";
|
|
4
5
|
import type { WandStorage } from "./storage.js";
|
|
5
6
|
import type { StructuredSessionManager } from "./structured-session-manager.js";
|
|
6
7
|
import type { ConversationTurn, ProcessEvent, SessionSnapshot, WandConfig } from "./types.js";
|
|
@@ -88,7 +89,7 @@ export declare class ConversationService {
|
|
|
88
89
|
/** Explicit resource action may prepare a channel; it never sends or starts a model. */
|
|
89
90
|
prepare(id: string, requestId: string, explicitCwd?: string): Promise<ConversationReceipt>;
|
|
90
91
|
private communicationChannel;
|
|
91
|
-
send(id: string, requestId: string, input: string, target: ConversationTarget, explicitCwd?: string, echo?: boolean, executionTarget?: ConversationExecutionTarget): Promise<ConversationReceipt>;
|
|
92
|
+
send(id: string, requestId: string, input: string, target: ConversationTarget, explicitCwd?: string, echo?: boolean, executionTarget?: ConversationExecutionTarget, executionPreference?: ConversationExecutionPreference): Promise<ConversationReceipt>;
|
|
92
93
|
/** One IM message is one fresh model context. No task, team, relay or previous DM history. */
|
|
93
94
|
private startMessageSession;
|
|
94
95
|
private requireTarget;
|
|
@@ -14,6 +14,7 @@ import { isGlobalWorkspace, normalizeProjectCwd, resolveWorkspaceIdForNewSession
|
|
|
14
14
|
import { CONVERSATION_SESSION_PREFIX, conversationSessionPreview } from "./conversation-session-preview.js";
|
|
15
15
|
import { conversationTaskLiveText } from "./conversation-task-preview.js";
|
|
16
16
|
import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
|
|
17
|
+
import { parseConversationExecutionPreference, withConversationExecutionPreference } from "./conversation-execution-preference.js";
|
|
17
18
|
import { userAuthor, userDisplayName } from "./user-profile.js";
|
|
18
19
|
import { cleanupWorktreeSync } from "./git-worktree.js";
|
|
19
20
|
import { provisionalTaskTitleFromDescription } from "./task-title.js";
|
|
@@ -621,13 +622,17 @@ export class ConversationService {
|
|
|
621
622
|
this.storage.saveConversation(instance);
|
|
622
623
|
return session;
|
|
623
624
|
}
|
|
624
|
-
send(id, requestId, input, target, explicitCwd, echo = true, executionTarget) {
|
|
625
|
+
send(id, requestId, input, target, explicitCwd, echo = true, executionTarget, executionPreference) {
|
|
626
|
+
const preference = parseConversationExecutionPreference(executionPreference);
|
|
625
627
|
return this.request(requestId, "send", { input, target, explicitCwd, echo,
|
|
628
|
+
...(preference ? { executionPreference: preference } : {}),
|
|
626
629
|
...(executionTarget?.workspaceId ? { workspaceId: executionTarget.workspaceId } : {}),
|
|
627
630
|
...(executionTarget?.title !== undefined ? { title: executionTarget.title } : {}),
|
|
628
631
|
...(executionTarget?.scheduled ? { scheduled: true } : {}) }, id, async (receipt, commit) => {
|
|
629
632
|
const text = shortText(input, "消息", 256_000, true);
|
|
630
633
|
const instance = this.requireInstance(id);
|
|
634
|
+
if (preference && (instance.kind !== "dm" || target))
|
|
635
|
+
throw new Error("临时执行配置仅用于私聊的新工作,不修改已有任务。");
|
|
631
636
|
let workspaceId;
|
|
632
637
|
if (executionTarget) {
|
|
633
638
|
if (instance.kind !== "dm" || target)
|
|
@@ -664,7 +669,7 @@ export class ConversationService {
|
|
|
664
669
|
return;
|
|
665
670
|
}
|
|
666
671
|
if (instance.kind === "dm") {
|
|
667
|
-
this.startMessageSession(instance, requestId, text, explicitCwd, receipt, commit, executionTarget ? { ...executionTarget, workspaceId } : undefined);
|
|
672
|
+
this.startMessageSession(instance, requestId, text, explicitCwd, receipt, commit, executionTarget ? { ...executionTarget, workspaceId } : undefined, preference);
|
|
668
673
|
return;
|
|
669
674
|
}
|
|
670
675
|
if (instance.kind === "group" && instance.team) {
|
|
@@ -708,9 +713,10 @@ export class ConversationService {
|
|
|
708
713
|
});
|
|
709
714
|
}
|
|
710
715
|
/** One IM message is one fresh model context. No task, team, relay or previous DM history. */
|
|
711
|
-
startMessageSession(instance, requestId, input, explicitCwd, receipt, commit, executionTarget) {
|
|
716
|
+
startMessageSession(instance, requestId, input, explicitCwd, receipt, commit, executionTarget, preference) {
|
|
712
717
|
requireTeamEmployee(this.storage, instance.peerEmployeeId ?? "");
|
|
713
|
-
const
|
|
718
|
+
const savedEmployee = this.storage.getSiliconEmployee(instance.peerEmployeeId);
|
|
719
|
+
const employee = preference ? withConversationExecutionPreference(savedEmployee, preference) : savedEmployee;
|
|
714
720
|
const selected = selectEmployeeCandidate(employee), agent = selected.agent;
|
|
715
721
|
const cwd = this.cwd(explicitCwd);
|
|
716
722
|
const model = agent.model === "default" ? fallbackProviderModel(this.deps.config, agent.provider) : agent.model;
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare class CoreTurnTracker {
|
|
5
5
|
private static activeTurns;
|
|
6
|
-
static
|
|
6
|
+
private static systemCallDrains;
|
|
7
|
+
/** Admission for one-shot SDK calls which do not enter the ordinary session input queue. */
|
|
8
|
+
static beginSystemCallDrain(): () => void;
|
|
9
|
+
static assertSystemCallAdmission(): void;
|
|
10
|
+
static startTurn(sessionId: string, systemCall?: boolean): symbol;
|
|
7
11
|
static endTurn(token: symbol): void;
|
|
8
12
|
static hasActiveTurns(): boolean;
|
|
9
13
|
static getActiveTurnCount(): number;
|
|
@@ -5,7 +5,23 @@ export class CoreTurnTracker {
|
|
|
5
5
|
// Each execution owns its token: a late cancelled turn cannot untrack its
|
|
6
6
|
// replacement in the same session.
|
|
7
7
|
static activeTurns = new Map();
|
|
8
|
-
static
|
|
8
|
+
static systemCallDrains = 0;
|
|
9
|
+
/** Admission for one-shot SDK calls which do not enter the ordinary session input queue. */
|
|
10
|
+
static beginSystemCallDrain() {
|
|
11
|
+
this.systemCallDrains++;
|
|
12
|
+
let released = false;
|
|
13
|
+
return () => { if (!released) {
|
|
14
|
+
released = true;
|
|
15
|
+
this.systemCallDrains--;
|
|
16
|
+
} };
|
|
17
|
+
}
|
|
18
|
+
static assertSystemCallAdmission() {
|
|
19
|
+
if (this.systemCallDrains > 0)
|
|
20
|
+
throw Object.assign(new Error("服务正在排空 Core,暂不接受新的 SDK 系统调用。"), { code: "SYSTEM_AI_DRAINING" });
|
|
21
|
+
}
|
|
22
|
+
static startTurn(sessionId, systemCall = false) {
|
|
23
|
+
if (systemCall)
|
|
24
|
+
this.assertSystemCallAdmission();
|
|
9
25
|
const token = Symbol(sessionId);
|
|
10
26
|
this.activeTurns.set(token, sessionId);
|
|
11
27
|
return token;
|
|
@@ -5,7 +5,9 @@ import type { SiliconEmployee } from "./ai-team-types.js";
|
|
|
5
5
|
import { type OpenRouterFreeModelsService } from "./openrouter-free-models.js";
|
|
6
6
|
import type { AiTextRequest, WandConfig } from "./types.js";
|
|
7
7
|
import type { WandTaskAgent } from "./task-types.js";
|
|
8
|
+
import { type AiCallTraceFactory } from "./system-ai-call.js";
|
|
8
9
|
interface DecisionExpertDeps {
|
|
10
|
+
trace?(employee: SiliconEmployee): AiCallTraceFactory;
|
|
9
11
|
employee(): SiliconEmployee | null;
|
|
10
12
|
local: Pick<DecisionService, "status" | "evaluate">;
|
|
11
13
|
free: Pick<OpenRouterFreeModelsService, "status" | "resolveForCall">;
|
|
@@ -3,6 +3,7 @@ import { decisionHardware } from "./decision-hardware.js";
|
|
|
3
3
|
import { DECISION_CANDIDATE_TIMEOUT_MS, DecisionError, parseDecisionRequest, parseDecisionResult } from "./decision-types.js";
|
|
4
4
|
import { isOpenRouterFreeSelector } from "./openrouter-free-models.js";
|
|
5
5
|
import { abortableEmployeeText, employeeTextCandidates, prepareEmployeeTextCandidate } from "./employee-text.js";
|
|
6
|
+
import { AiCallTraceError } from "./system-ai-call.js";
|
|
6
7
|
/** Bounded decisions use the employee's ordered adapters; LAYA stays a local evaluator. */
|
|
7
8
|
export class DecisionExpertService {
|
|
8
9
|
deps;
|
|
@@ -47,7 +48,9 @@ export class DecisionExpertService {
|
|
|
47
48
|
system: `${employee.prompt}\n只输出JSON对象 {"answers":{...}}。对每题保留type;choice给choice和所有criteria的probabilities(总和1),score给score和以0开始索引的probabilities(总和1),noul给0到1的noul。不得执行操作或增加候选。概率是实验性辅助而非授权。`,
|
|
48
49
|
prompt: JSON.stringify(request),
|
|
49
50
|
};
|
|
51
|
+
let trace;
|
|
50
52
|
try {
|
|
53
|
+
trace = this.deps.trace?.(employee).start("decision", prompt, this.deps.config.defaultCwd);
|
|
51
54
|
for (const { agent, index: candidate } of candidates) {
|
|
52
55
|
cancelled();
|
|
53
56
|
const deadline = new AbortController();
|
|
@@ -62,6 +65,7 @@ export class DecisionExpertService {
|
|
|
62
65
|
|| !hardware.suitable || !local.enabled || !local.configured || !local.supported) {
|
|
63
66
|
throw new DecisionError("UNAVAILABLE", "LAYA 不可用。", 503);
|
|
64
67
|
}
|
|
68
|
+
trace?.localStarted(agent.model);
|
|
65
69
|
stage = "execution";
|
|
66
70
|
executed = true;
|
|
67
71
|
const localResult = await abortableEmployeeText(this.deps.local.evaluate(request, caller, attempt), attempt);
|
|
@@ -69,8 +73,11 @@ export class DecisionExpertService {
|
|
|
69
73
|
const result = parseDecisionResult(localResult, request, { model: localResult.model, runtime: "laya-mlx" });
|
|
70
74
|
cancelled();
|
|
71
75
|
attempt.throwIfAborted();
|
|
76
|
+
trace?.candidateSucceeded(JSON.stringify(localResult), result);
|
|
77
|
+
trace?.complete();
|
|
72
78
|
return { ...result, executor: { employeeId: employee.id, candidate, source: "local" } };
|
|
73
79
|
}
|
|
80
|
+
trace?.candidateStarted(agent);
|
|
74
81
|
let run;
|
|
75
82
|
if (this.deps.generate) {
|
|
76
83
|
run = () => this.deps.generate(agent, request, employee, attempt);
|
|
@@ -83,7 +90,7 @@ export class DecisionExpertService {
|
|
|
83
90
|
let usage;
|
|
84
91
|
try {
|
|
85
92
|
generate = await abortableEmployeeText((this.deps.prepare ?? ((next, text, checkedSignal) => prepareEmployeeTextCandidate({
|
|
86
|
-
config: this.deps.config, free: this.deps.free, onUsage: value => { usage = value; },
|
|
93
|
+
config: this.deps.config, free: this.deps.free, callSessionId: trace?.sessionId, onUsage: value => { usage = value; trace?.usageReported(value); },
|
|
87
94
|
}, next, text, checkedSignal)))(agent, prompt, check), check);
|
|
88
95
|
}
|
|
89
96
|
finally {
|
|
@@ -108,10 +115,15 @@ export class DecisionExpertService {
|
|
|
108
115
|
attempt.throwIfAborted();
|
|
109
116
|
if (generated.available === false)
|
|
110
117
|
result.usage.available = false;
|
|
118
|
+
trace?.candidateSucceeded(JSON.stringify(generated.answers), result);
|
|
119
|
+
trace?.complete();
|
|
111
120
|
return { ...result, executor: { employeeId: employee.id, candidate,
|
|
112
121
|
source: isOpenRouterFreeSelector(agent.model) ? "free-group" : agent.engine === "sdk" ? "sdk" : "cli" } };
|
|
113
122
|
}
|
|
114
123
|
catch (error) {
|
|
124
|
+
if (error instanceof AiCallTraceError)
|
|
125
|
+
throw error;
|
|
126
|
+
trace?.candidateFailed(agent, error, stage);
|
|
115
127
|
cancelled();
|
|
116
128
|
const timedOut = deadline.signal.aborted || error instanceof DecisionError && error.code === "TIMEOUT";
|
|
117
129
|
sawTimeout ||= timedOut;
|
|
@@ -130,6 +142,10 @@ export class DecisionExpertService {
|
|
|
130
142
|
const code = timedOut ? "TIMEOUT" : executed ? "EXECUTION_FAILED" : "CONFIGURATION_REQUIRED";
|
|
131
143
|
throw new DecisionError(code, `${employee.name}已尝试全部配置的决策候选,均未成功${failures.length ? `(${failures.join(";").slice(0, 2048)})` : ""};请检查该员工调用链。`, timedOut ? 504 : 503);
|
|
132
144
|
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
trace?.fail(error, signal?.aborted === true);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
133
149
|
finally {
|
|
134
150
|
this.active -= 1;
|
|
135
151
|
}
|
package/dist/decision-skill.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type SessionProvider } from "./provider-catalog.js";
|
|
2
|
-
/** One managed source, three discovery locations; never edits a provider's settings or permissions. */
|
|
3
2
|
export declare function installDecisionSkill(configDir: string, providers?: readonly SessionProvider[], home?: string): Promise<{
|
|
4
3
|
installed: string[];
|
|
5
4
|
providers: readonly SessionProvider[];
|
package/dist/decision-skill.js
CHANGED
|
@@ -1,70 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFile, mkdir, lstat, readlink, writeFile, symlink, realpath } from "node:fs/promises";
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { installManagedSkill } from "./managed-skill.js";
|
|
6
6
|
import { SESSION_PROVIDERS } from "./provider-catalog.js";
|
|
7
|
-
const FILES = ["SKILL.md", "scripts/decide.mjs"];
|
|
8
|
-
const sha = (data) => createHash("sha256").update(data).digest("hex");
|
|
9
|
-
/** One managed source, three discovery locations; never edits a provider's settings or permissions. */
|
|
10
7
|
export async function installDecisionSkill(configDir, providers = SESSION_PROVIDERS, home = homedir()) {
|
|
11
|
-
if (!providers.length || providers.some((provider) => !SESSION_PROVIDERS.includes(provider)))
|
|
12
|
-
throw new Error("未知CLI技能目标。");
|
|
13
|
-
const canonical = path.join(configDir, "skills", "wand-decision");
|
|
14
8
|
const source = fileURLToPath(new URL("../skills/wand-decision/", import.meta.url));
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const directory = provider === "claude" ? ".claude" : provider === "qoder" ? ".qoder" : ".agents";
|
|
18
|
-
targets.add(path.join(home, directory, "skills", "wand-decision"));
|
|
19
|
-
}
|
|
20
|
-
// Preflight every target before touching anything; existing user content is never replaced.
|
|
21
|
-
for (const target of targets) {
|
|
22
|
-
try {
|
|
23
|
-
const info = await lstat(target);
|
|
24
|
-
if (!info.isSymbolicLink() || path.resolve(path.dirname(target), await readlink(target)) !== path.resolve(canonical)) {
|
|
25
|
-
throw new Error(`已有同名技能,保留原内容,请先手动处理:${target}`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
if (error.code !== "ENOENT")
|
|
30
|
-
throw error;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
let previous = {};
|
|
34
|
-
try {
|
|
35
|
-
previous = JSON.parse(await readFile(path.join(canonical, ".wand-managed.json"), "utf8"));
|
|
36
|
-
}
|
|
37
|
-
catch (error) {
|
|
38
|
-
if (error.code !== "ENOENT")
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
const contents = await Promise.all(FILES.map(async (file) => ({ file, content: await readFile(path.join(source, file), "utf8") })));
|
|
42
|
-
for (const { file, content } of contents) {
|
|
43
|
-
try {
|
|
44
|
-
const old = await readFile(path.join(canonical, file), "utf8");
|
|
45
|
-
if (old !== content && sha(old) !== previous[file])
|
|
46
|
-
throw new Error(`技能已有用户修改,拒绝覆盖:${file}`);
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
if (error.code !== "ENOENT")
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
await mkdir(path.join(canonical, "scripts"), { recursive: true });
|
|
54
|
-
for (const { file, content } of contents)
|
|
55
|
-
await writeFile(path.join(canonical, file), content, { mode: 0o644 });
|
|
56
|
-
await writeFile(path.join(canonical, ".wand-managed.json"), JSON.stringify(Object.fromEntries(contents.map(({ file, content }) => [file, sha(content)]))), { mode: 0o600 });
|
|
57
|
-
for (const target of targets) {
|
|
58
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
59
|
-
try {
|
|
60
|
-
await symlink(canonical, target, "dir");
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
if (error.code !== "EEXIST")
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
66
|
-
if (await realpath(target) !== await realpath(canonical))
|
|
67
|
-
throw new Error("技能链接目标校验失败。");
|
|
68
|
-
}
|
|
69
|
-
return { installed: [...targets], providers };
|
|
9
|
+
const files = Object.fromEntries(await Promise.all(["SKILL.md", "scripts/decide.mjs"].map(async (file) => [file, await readFile(path.join(source, file), "utf8")])));
|
|
10
|
+
return installManagedSkill(configDir, "wand-decision", files, providers, home);
|
|
70
11
|
}
|
package/dist/deployment-lock.js
CHANGED
|
@@ -43,7 +43,10 @@ export function acquireDeploymentLock(value, options = {}) {
|
|
|
43
43
|
catch (error) {
|
|
44
44
|
if (error.code !== "EEXIST")
|
|
45
45
|
throw error;
|
|
46
|
-
|
|
46
|
+
const ownerState = Number.isSafeInteger(owner?.pid) && owner.pid > 0
|
|
47
|
+
? pidAlive(owner.pid) ? "进程仍存活" : "进程已退出,可能存在遗留写者或未完成事务"
|
|
48
|
+
: "归属未知";
|
|
49
|
+
throw new Error(`另一个 wand 更新正在进行中或遗留安装锁尚未核实(锁: ${lockPath}${typeof owner?.pid === "number" ? `, PID ${owner.pid}` : ""};${ownerState})。旧安装未修改;未自动删除锁,请先核查安装子进程、备份与事务状态。`);
|
|
47
50
|
}
|
|
48
51
|
try {
|
|
49
52
|
writeFileSync(path.join(lockPath, "owner.json"), `${JSON.stringify({ pid: ownerPid, token, createdAt: Date.now() })}\n`, { mode: 0o600 });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type EmployeeImConfig, type EmployeeImConfigInput } from "./employee-im-types.js";
|
|
2
|
+
/** Server-only write-only credentials, never included in employee/session projections. */
|
|
3
|
+
export interface EmployeeImCredentials {
|
|
4
|
+
botToken?: string;
|
|
5
|
+
webhook?: string;
|
|
6
|
+
secret?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface EmployeeImSettings {
|
|
9
|
+
config: EmployeeImConfig;
|
|
10
|
+
credentials: EmployeeImCredentials;
|
|
11
|
+
}
|
|
12
|
+
export declare class EmployeeImConfigError extends Error {
|
|
13
|
+
}
|
|
14
|
+
export declare function dingTalkWebhook(value: string): URL;
|
|
15
|
+
/** Older draft configs had per-channel events. Read them as one union without rewriting storage on GET. */
|
|
16
|
+
export declare function normalizeStoredEmployeeImConfig(value: unknown): EmployeeImConfig;
|
|
17
|
+
export declare function parseEmployeeImConfig(input: EmployeeImConfigInput | unknown, previous: EmployeeImSettings): EmployeeImSettings;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { EMPLOYEE_IM_EVENTS } from "./employee-im-types.js";
|
|
2
|
+
export class EmployeeImConfigError extends Error {
|
|
3
|
+
}
|
|
4
|
+
const knownEvents = new Set(EMPLOYEE_IM_EVENTS.map(event => event.value));
|
|
5
|
+
function object(value) {
|
|
6
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
7
|
+
throw new EmployeeImConfigError("IM 配置必须是对象。");
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function credential(value, previous, label, max) {
|
|
11
|
+
if (value === undefined || value === "")
|
|
12
|
+
return previous;
|
|
13
|
+
if (value === null)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (typeof value !== "string" || value.length > max || /[\x00-\x20\x7f]/.test(value))
|
|
16
|
+
throw new EmployeeImConfigError(`${label}格式无效。`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
export function dingTalkWebhook(value) {
|
|
20
|
+
let url;
|
|
21
|
+
try {
|
|
22
|
+
url = new URL(value);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new EmployeeImConfigError("请填写有效的钉钉机器人 Webhook。");
|
|
26
|
+
}
|
|
27
|
+
const token = url.searchParams.get("access_token");
|
|
28
|
+
if (url.protocol !== "https:" || url.hostname !== "oapi.dingtalk.com" || url.port || url.username || url.password
|
|
29
|
+
|| url.pathname !== "/robot/send" || url.hash || !token || !/^[A-Za-z0-9_-]{8,256}$/.test(token)
|
|
30
|
+
|| [...url.searchParams.keys()].some(key => key !== "access_token") || url.searchParams.getAll("access_token").length !== 1) {
|
|
31
|
+
throw new EmployeeImConfigError("仅支持 https://oapi.dingtalk.com/robot/send?access_token=… 官方机器人地址。");
|
|
32
|
+
}
|
|
33
|
+
return url;
|
|
34
|
+
}
|
|
35
|
+
/** Older draft configs had per-channel events. Read them as one union without rewriting storage on GET. */
|
|
36
|
+
export function normalizeStoredEmployeeImConfig(value) {
|
|
37
|
+
const raw = object(value), telegram = object(raw.telegram), dingtalk = object(raw.dingtalk);
|
|
38
|
+
const source = Array.isArray(raw.events) ? raw.events
|
|
39
|
+
: [...(Array.isArray(telegram.events) ? telegram.events : []), ...(Array.isArray(dingtalk.events) ? dingtalk.events : [])];
|
|
40
|
+
return {
|
|
41
|
+
events: [...new Set(source.filter((event) => typeof event === "string" && knownEvents.has(event)))],
|
|
42
|
+
telegram: { enabled: telegram.enabled === true, chatId: typeof telegram.chatId === "string" ? telegram.chatId : "", botTokenConfigured: telegram.botTokenConfigured === true },
|
|
43
|
+
dingtalk: { enabled: dingtalk.enabled === true, webhookConfigured: dingtalk.webhookConfigured === true, secretConfigured: dingtalk.secretConfigured === true },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function parseEmployeeImConfig(input, previous) {
|
|
47
|
+
const body = object(input);
|
|
48
|
+
const events = body.events === undefined ? previous.config.events : body.events;
|
|
49
|
+
if (!Array.isArray(events) || events.length > EMPLOYEE_IM_EVENTS.length
|
|
50
|
+
|| events.some(event => typeof event !== "string" || !knownEvents.has(event)))
|
|
51
|
+
throw new EmployeeImConfigError("通知节点无效。");
|
|
52
|
+
const config = { events: [...new Set(events)] };
|
|
53
|
+
const credentials = { ...previous.credentials };
|
|
54
|
+
for (const provider of ["telegram", "dingtalk"]) {
|
|
55
|
+
const raw = object(body[provider]);
|
|
56
|
+
if (typeof raw.enabled !== "boolean")
|
|
57
|
+
throw new EmployeeImConfigError("请选择是否启用 IM 通知。");
|
|
58
|
+
config[provider] = { enabled: raw.enabled };
|
|
59
|
+
if (provider === "telegram") {
|
|
60
|
+
if (raw.chatId !== undefined && typeof raw.chatId !== "string")
|
|
61
|
+
throw new EmployeeImConfigError("Telegram Chat ID 格式无效。");
|
|
62
|
+
const chatId = raw.chatId?.trim() ?? previous.config.telegram.chatId ?? "";
|
|
63
|
+
if (chatId && !/^(?:-?\d{1,24}|@[A-Za-z0-9_]{5,32})$/.test(chatId))
|
|
64
|
+
throw new EmployeeImConfigError("Telegram Chat ID 格式无效。");
|
|
65
|
+
config.telegram.chatId = chatId;
|
|
66
|
+
credentials.botToken = credential(raw.botToken, credentials.botToken, "Telegram Bot Token", 256);
|
|
67
|
+
if (credentials.botToken && !/^\d{5,20}:[A-Za-z0-9_-]{20,200}$/.test(credentials.botToken))
|
|
68
|
+
throw new EmployeeImConfigError("Telegram Bot Token 格式无效。");
|
|
69
|
+
if (raw.enabled && (!credentials.botToken || !chatId))
|
|
70
|
+
throw new EmployeeImConfigError("启用 Telegram 前请填写 Bot Token 和 Chat ID。");
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
credentials.webhook = credential(raw.webhook, credentials.webhook, "钉钉 Webhook", 1024);
|
|
74
|
+
credentials.secret = credential(raw.secret, credentials.secret, "钉钉签名密钥", 512);
|
|
75
|
+
if (credentials.webhook)
|
|
76
|
+
dingTalkWebhook(credentials.webhook);
|
|
77
|
+
if (raw.enabled && !credentials.webhook)
|
|
78
|
+
throw new EmployeeImConfigError("启用钉钉前请填写机器人 Webhook。");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
config.telegram.botTokenConfigured = !!credentials.botToken;
|
|
82
|
+
config.dingtalk.webhookConfigured = !!credentials.webhook;
|
|
83
|
+
config.dingtalk.secretConfigured = !!credentials.secret;
|
|
84
|
+
return { config, credentials };
|
|
85
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AiTeamRun, AiTeamStep } from "./ai-team-types.js";
|
|
2
|
+
import type { EmployeeImEvent } from "./employee-im-types.js";
|
|
3
|
+
import type { ProcessEvent, SessionSnapshot } from "./types.js";
|
|
4
|
+
interface Notifier {
|
|
5
|
+
notify(employeeId: string, event: EmployeeImEvent, context: {
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
}): void;
|
|
9
|
+
}
|
|
10
|
+
interface Options {
|
|
11
|
+
im: Notifier;
|
|
12
|
+
getSession(id: string): SessionSnapshot | null;
|
|
13
|
+
getRunSteps?(runId: string): AiTeamStep[];
|
|
14
|
+
}
|
|
15
|
+
/** Read-only projection of execution facts. Never parses assistant prose or synthesizes success from idle. */
|
|
16
|
+
export declare class EmployeeImLifecycle {
|
|
17
|
+
private readonly options;
|
|
18
|
+
private readonly sessions;
|
|
19
|
+
private readonly runs;
|
|
20
|
+
private readonly pending;
|
|
21
|
+
private disposed;
|
|
22
|
+
constructor(options: Options);
|
|
23
|
+
/** Existing/recovered state is a baseline, not a new notification subscription replay. */
|
|
24
|
+
seed(sessions: SessionSnapshot[], runs?: AiTeamRun[]): void;
|
|
25
|
+
ingest(event: ProcessEvent): void;
|
|
26
|
+
private project;
|
|
27
|
+
/** Called at the runner's durable run/step change boundary, including async decisions after session end. */
|
|
28
|
+
ingestRun(run: AiTeamRun): void;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
private readRun;
|
|
31
|
+
private read;
|
|
32
|
+
private remember;
|
|
33
|
+
private send;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { enrichStructuredMessages } from "./structured-client-protocol.js";
|
|
2
|
+
/** Read-only projection of execution facts. Never parses assistant prose or synthesizes success from idle. */
|
|
3
|
+
export class EmployeeImLifecycle {
|
|
4
|
+
options;
|
|
5
|
+
sessions = new Map();
|
|
6
|
+
runs = new Map();
|
|
7
|
+
pending = new Map();
|
|
8
|
+
disposed = false;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
/** Existing/recovered state is a baseline, not a new notification subscription replay. */
|
|
13
|
+
seed(sessions, runs = []) {
|
|
14
|
+
for (const session of sessions)
|
|
15
|
+
if (session.employeeId)
|
|
16
|
+
this.sessions.set(session.id, { ...this.read(session), semanticPrimed: false });
|
|
17
|
+
for (const run of runs)
|
|
18
|
+
this.runs.set(run.id, this.readRun(run));
|
|
19
|
+
}
|
|
20
|
+
ingest(event) {
|
|
21
|
+
if (this.disposed || event.sessionId === "__system__" || event.type === "resync" || event.type === "notification" || event.type === "usage")
|
|
22
|
+
return;
|
|
23
|
+
// Text chunks are high-frequency; coalesce their semantic projection. Terminal/status facts are immediate.
|
|
24
|
+
if (event.type === "output") {
|
|
25
|
+
const data = event.data && typeof event.data === "object" ? event.data : {};
|
|
26
|
+
if (Array.isArray(data.queuedMessages) && data.queuedMessages.length !== (this.sessions.get(event.sessionId)?.queue ?? 0))
|
|
27
|
+
this.project(event.sessionId);
|
|
28
|
+
if (!this.pending.has(event.sessionId) && this.pending.size < 1000) {
|
|
29
|
+
const timer = setTimeout(() => { this.pending.delete(event.sessionId); this.project(event.sessionId); }, 100);
|
|
30
|
+
timer.unref?.();
|
|
31
|
+
this.pending.set(event.sessionId, timer);
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const timer = this.pending.get(event.sessionId);
|
|
36
|
+
if (timer) {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
this.pending.delete(event.sessionId);
|
|
39
|
+
}
|
|
40
|
+
const data = event.data && typeof event.data === "object" ? event.data : {};
|
|
41
|
+
this.project(event.sessionId, typeof data.completionRevision === "number" ? data.completionRevision : undefined, data.turnOutcome === "stopped");
|
|
42
|
+
}
|
|
43
|
+
project(id, revision, explicitlyStopped = false) {
|
|
44
|
+
if (this.disposed)
|
|
45
|
+
return;
|
|
46
|
+
const snapshot = this.options.getSession(id);
|
|
47
|
+
if (!snapshot?.employeeId)
|
|
48
|
+
return;
|
|
49
|
+
const next = this.read(snapshot);
|
|
50
|
+
const previous = this.sessions.get(id);
|
|
51
|
+
next.completion = Math.max(next.completion, revision ?? 0, previous?.completion ?? 0);
|
|
52
|
+
next.stopNotified = next.active ? false : previous?.stopNotified ?? false;
|
|
53
|
+
this.remember(this.sessions, id, next);
|
|
54
|
+
if (!previous && snapshot.autoRecovered)
|
|
55
|
+
return;
|
|
56
|
+
// A changed employee binding is a fresh baseline; never attribute another employee's old result.
|
|
57
|
+
if (previous && previous.employeeId !== snapshot.employeeId)
|
|
58
|
+
return;
|
|
59
|
+
const send = (event) => this.send(snapshot.employeeId, event, { sessionId: id, title: snapshot.title || snapshot.currentTaskTitle || "员工任务" });
|
|
60
|
+
if (next.active && (!previous?.active || (next.turn !== previous.turn && previous.turn !== "-1:")))
|
|
61
|
+
send("task_started");
|
|
62
|
+
if (next.permission && next.permission !== previous?.permission)
|
|
63
|
+
send("waiting_permission");
|
|
64
|
+
if (previous?.semanticPrimed !== false && [...next.questions].some(key => !previous?.questions.has(key)))
|
|
65
|
+
send("waiting_input");
|
|
66
|
+
if (next.queue > (previous?.queue ?? 0))
|
|
67
|
+
send("message_queued");
|
|
68
|
+
if (previous && next.candidate !== previous.candidate)
|
|
69
|
+
send("candidate_switched");
|
|
70
|
+
if (previous && next.completion > previous.completion)
|
|
71
|
+
send("task_completed");
|
|
72
|
+
if (next.outcome && next.outcome !== previous?.outcome) {
|
|
73
|
+
send(snapshot.status === "stopped" ? "task_stopped" : "task_failed");
|
|
74
|
+
if (snapshot.status === "stopped")
|
|
75
|
+
next.stopNotified = true;
|
|
76
|
+
}
|
|
77
|
+
if (explicitlyStopped && !next.stopNotified) {
|
|
78
|
+
send("task_stopped");
|
|
79
|
+
next.stopNotified = true;
|
|
80
|
+
}
|
|
81
|
+
if (previous?.semanticPrimed !== false && [...next.steps].some(([key, status]) => status === "completed"
|
|
82
|
+
&& (next.turn !== previous?.turn || previous?.steps.get(key) !== "completed")))
|
|
83
|
+
send("step_completed");
|
|
84
|
+
}
|
|
85
|
+
/** Called at the runner's durable run/step change boundary, including async decisions after session end. */
|
|
86
|
+
ingestRun(run) {
|
|
87
|
+
if (this.disposed)
|
|
88
|
+
return;
|
|
89
|
+
const next = this.readRun(run);
|
|
90
|
+
const previous = this.runs.get(run.id);
|
|
91
|
+
this.remember(this.runs, run.id, next);
|
|
92
|
+
const leader = run.team.members.find(member => member.isLeader)?.employeeId;
|
|
93
|
+
const context = { sessionId: run.chatSessionId ?? undefined, title: run.team.name };
|
|
94
|
+
if (leader && previous?.status !== run.status) {
|
|
95
|
+
const event = { running: "team_started", done: "team_completed", failed: "team_failed", stopped: "team_stopped",
|
|
96
|
+
waiting_user: "waiting_input", awaiting_approval: "waiting_permission" }[run.status];
|
|
97
|
+
this.send(leader, event, context);
|
|
98
|
+
}
|
|
99
|
+
for (const step of this.options.getRunSteps?.(run.id) ?? []) {
|
|
100
|
+
if (step.status !== "done" || previous?.steps.get(step.id) === "done")
|
|
101
|
+
continue;
|
|
102
|
+
const employeeId = run.team.members.find(member => member.id === step.memberId)?.employeeId;
|
|
103
|
+
if (employeeId)
|
|
104
|
+
this.send(employeeId, "step_completed", { sessionId: step.sessionId ?? undefined, title: step.title });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
dispose() {
|
|
108
|
+
this.disposed = true;
|
|
109
|
+
for (const timer of this.pending.values())
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
this.pending.clear();
|
|
112
|
+
this.sessions.clear();
|
|
113
|
+
this.runs.clear();
|
|
114
|
+
}
|
|
115
|
+
readRun(run) {
|
|
116
|
+
return { status: run.status, steps: new Map((this.options.getRunSteps?.(run.id) ?? []).map(step => [step.id, step.status])) };
|
|
117
|
+
}
|
|
118
|
+
read(session) {
|
|
119
|
+
const active = session.status === "running" && (session.sessionKind === "structured" ? session.structuredState?.inFlight === true : session.ptyBusy === true);
|
|
120
|
+
// Only the current user turn participates; bound work on very large histories.
|
|
121
|
+
const messages = session.messages ?? [];
|
|
122
|
+
let start = messages.length - 1;
|
|
123
|
+
while (start >= 0 && !(messages[start].role === "user" && messages[start].content.some(block => block.type === "text")))
|
|
124
|
+
start -= 1;
|
|
125
|
+
const segment = messages.slice(Math.max(start, messages.length - 200, 0));
|
|
126
|
+
const results = new Map(segment.flatMap(turn => turn.content.flatMap(block => block.type === "tool_result" ? [[block.tool_use_id, !block.is_error]] : [])));
|
|
127
|
+
// Do not notify an intended TaskUpdate/TodoWrite before its successful result.
|
|
128
|
+
// Codex's native plan updates are translated by its protocol adapter into result-less TodoWrite snapshots.
|
|
129
|
+
const acknowledged = segment.map(turn => ({ ...turn, content: turn.content.filter(block => block.type !== "tool_use"
|
|
130
|
+
|| block.name === "AskUserQuestion" || results.get(block.id) === true
|
|
131
|
+
|| (session.provider === "codex" && block.name === "TodoWrite" && !results.has(block.id))) }));
|
|
132
|
+
const current = enrichStructuredMessages(acknowledged);
|
|
133
|
+
const answered = new Set(results.keys());
|
|
134
|
+
const questions = new Set();
|
|
135
|
+
const steps = new Map();
|
|
136
|
+
for (const turn of current)
|
|
137
|
+
for (const block of turn.content) {
|
|
138
|
+
if (block.type !== "tool_use")
|
|
139
|
+
continue;
|
|
140
|
+
if (active && block.semantic?.kind === "question_request" && !answered.has(block.id))
|
|
141
|
+
questions.add(block.id);
|
|
142
|
+
if (block.semantic?.kind === "task_list")
|
|
143
|
+
for (const item of block.semantic.items)
|
|
144
|
+
steps.set(item.id, item.status);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
semanticPrimed: true, stopNotified: false, employeeId: session.employeeId, active,
|
|
148
|
+
permission: session.pendingEscalation?.requestId ?? (session.permissionBlocked ? "blocked" : ""), questions,
|
|
149
|
+
queue: session.queuedMessages?.length ?? 0, candidate: session.employeeCandidateIndex ?? 0,
|
|
150
|
+
completion: session.completionRevision ?? 0,
|
|
151
|
+
outcome: session.status === "failed" || session.status === "stopped" ? `${session.status}:${session.endedAt ?? ""}`
|
|
152
|
+
: session.providerCliActive === false && typeof session.providerCliExitCode === "number" && session.providerCliExitCode !== 0 ? `cli-failed:${session.providerCliExitCode}` : "",
|
|
153
|
+
turn: `${start}:${messages[start]?.createdAt ?? ""}`, steps,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
remember(map, key, value) {
|
|
157
|
+
map.delete(key);
|
|
158
|
+
map.set(key, value);
|
|
159
|
+
if (map.size > 10_000)
|
|
160
|
+
map.delete(map.keys().next().value);
|
|
161
|
+
}
|
|
162
|
+
send(employeeId, event, context) {
|
|
163
|
+
try {
|
|
164
|
+
this.options.im.notify(employeeId, event, { sessionId: context.sessionId, title: context.title?.slice(0, 200) });
|
|
165
|
+
}
|
|
166
|
+
catch { /* IM is never an execution outcome */ }
|
|
167
|
+
}
|
|
168
|
+
}
|