pi-dag-workflow 0.0.0-stage → 0.7.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/CHANGELOG.md +454 -0
- package/LICENSE +21 -0
- package/README.md +211 -2
- package/README.zh-CN.md +212 -0
- package/docs/COMMUNICATION.md +129 -0
- package/docs/RELEASING.md +66 -0
- package/package.json +70 -4
- package/src/agents/commands/child.ts +148 -0
- package/src/agents/commands/register.ts +404 -0
- package/src/agents/domain/context.ts +183 -0
- package/src/agents/domain/diagnostics.ts +32 -0
- package/src/agents/domain/owner.ts +448 -0
- package/src/agents/domain/profiles.ts +216 -0
- package/src/agents/domain/runtime.ts +603 -0
- package/src/agents/domain/schema.ts +15 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/io/contracts.ts +50 -0
- package/src/agents/io/host.ts +16 -0
- package/src/agents/io/notices.ts +27 -0
- package/src/agents/io/persistence.ts +210 -0
- package/src/agents/io/process.ts +100 -0
- package/src/agents/io/profile-file.ts +16 -0
- package/src/agents/io/skills.ts +46 -0
- package/src/goal/commands/register.ts +235 -0
- package/src/goal/domain/failure.ts +14 -0
- package/src/goal/domain/owner.ts +575 -0
- package/src/goal/domain/policy.ts +8 -0
- package/src/goal/domain/prompt.ts +57 -0
- package/src/goal/domain/state.ts +176 -0
- package/src/goal/index.ts +5 -0
- package/src/goal/io/host.ts +8 -0
- package/src/plan/commands/register.ts +101 -0
- package/src/plan/domain/owner.ts +51 -0
- package/src/plan/domain/policy.ts +27 -0
- package/src/plan/domain/state.ts +15 -0
- package/src/plan/index.ts +13 -0
- package/src/plan/io/records.ts +3 -0
- package/src/retry/commands/menu.ts +158 -0
- package/src/retry/commands/register.ts +26 -0
- package/src/retry/domain/owner.ts +418 -0
- package/src/retry/domain/state.ts +71 -0
- package/src/retry/index.ts +5 -0
- package/src/retry/io/host.ts +15 -0
- package/src/shared/agent-view.ts +18 -0
- package/src/shared/completion.ts +111 -0
- package/src/shared/config-file.ts +106 -0
- package/src/shared/config.ts +112 -0
- package/src/shared/dag/cache.ts +26 -0
- package/src/shared/dag/graph.ts +170 -0
- package/src/shared/dag/layout.ts +130 -0
- package/src/shared/goal-view.ts +2 -0
- package/src/shared/i18n.ts +62 -0
- package/src/shared/message-types.ts +12 -0
- package/src/shared/messages.en.ts +1290 -0
- package/src/shared/passive-message.ts +50 -0
- package/src/shared/pi-cli.ts +29 -0
- package/src/shared/profile-launch.ts +110 -0
- package/src/shared/state-types.ts +5 -0
- package/src/shared/task-schema.ts +39 -0
- package/src/shared/task-types.ts +25 -0
- package/src/shared/terminal.ts +19 -0
- package/src/shared/tool-info.ts +8 -0
- package/src/shared/ui/filter.ts +144 -0
- package/src/shared/ui/menu.ts +447 -0
- package/src/shared/ui/panel.ts +768 -0
- package/src/shared/ui/presentation.ts +624 -0
- package/src/shared/ui/render-cache.ts +63 -0
- package/src/shared/ui/render.ts +641 -0
- package/src/shared/ui/symbols.ts +136 -0
- package/src/team/commands/bootstrap.ts +52 -0
- package/src/team/commands/controls.ts +91 -0
- package/src/team/commands/history.ts +100 -0
- package/src/team/commands/menu.ts +455 -0
- package/src/team/commands/register.ts +1321 -0
- package/src/team/domain/inbox.ts +178 -0
- package/src/team/domain/launcher.ts +355 -0
- package/src/team/domain/lifecycle.ts +34 -0
- package/src/team/domain/teams.ts +161 -0
- package/src/team/index.ts +4 -0
- package/src/team/io/inbox.ts +255 -0
- package/src/team/io/launcher.ts +270 -0
- package/src/team/io/lock.ts +19 -0
- package/src/team/io/registry.ts +477 -0
- package/src/team/io/runtime.ts +353 -0
- package/src/team/io/session-activity.ts +185 -0
- package/src/team/io/session-delete.ts +98 -0
- package/src/team/io/session-history.ts +153 -0
- package/src/team/io/session-tail.ts +132 -0
- package/src/team/io/sid-lock.ts +115 -0
- package/src/team/io/sid-session.ts +98 -0
- package/src/todos/commands/checkpoints.ts +31 -0
- package/src/todos/commands/register.ts +395 -0
- package/src/todos/domain/checkpoint-state.ts +32 -0
- package/src/todos/domain/checkpoint-summary.ts +146 -0
- package/src/todos/domain/cleanup.ts +25 -0
- package/src/todos/domain/owner.ts +109 -0
- package/src/todos/domain/presets.ts +192 -0
- package/src/todos/domain/state.ts +287 -0
- package/src/todos/index.ts +15 -0
- package/src/todos/io/records.ts +8 -0
- package/src/ui/index.ts +4 -0
- package/src/workflow/commands/connect.ts +81 -0
- package/src/workflow/commands/view.ts +121 -0
- package/src/workflow/domain/features.ts +5 -0
- package/src/workflow/domain/view-owner.ts +18 -0
- package/src/workflow/domain/view-state.ts +12 -0
- package/src/workflow/index.ts +225 -0
- package/src/workflow/io/session.ts +48 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext, SessionBoundaryDraft } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import { configPaths, resolveChildExtensionPaths } from '../../shared/config.ts';
|
|
4
|
+
import { AgentRuntime, type JobSummary } from './runtime.ts';
|
|
5
|
+
import { CORE_TOOLS, CHILD_TOOLS, ProfileStore, readProfileExtensionPaths } from './profiles.ts';
|
|
6
|
+
import { contextArtifacts, taskContext } from './context.ts';
|
|
7
|
+
import { AgentNotices, type Notice } from '../io/notices.ts';
|
|
8
|
+
import { clean, notify } from '../../shared/terminal.ts';
|
|
9
|
+
import type { TodoParams } from '../../shared/task-schema.ts';
|
|
10
|
+
import type { ClearScope, Todo, WorkflowState } from '../../shared/task-types.ts';
|
|
11
|
+
import { workflowNamespace } from '../../shared/tool-info.ts';
|
|
12
|
+
import { AGENT_REPORT_MESSAGE } from '../../shared/message-types.ts';
|
|
13
|
+
import { chinese, type Translator } from '../../shared/i18n.ts';
|
|
14
|
+
import { AgentJournal, AGENTS_ENTRY_TYPE, foldAgentEntries } from '../io/persistence.ts';
|
|
15
|
+
import { agentsIO } from '../io/host.ts';
|
|
16
|
+
import type { AgentSpawnParams, AgentInspectParams, AgentSendParams, AgentCancelParams } from './schema.ts';
|
|
17
|
+
|
|
18
|
+
export const AGENTS_TYPE = AGENTS_ENTRY_TYPE;
|
|
19
|
+
function availableModels(ctx: ExtensionContext) {
|
|
20
|
+
const compare = (a: string, b: string) => a < b ? -1 : a > b ? 1 : 0;
|
|
21
|
+
return ctx.modelRegistry.getAvailable()
|
|
22
|
+
// A partially described registry entry must not break the whole panel: only arrays are copied.
|
|
23
|
+
.map(({ provider, id, name, reasoning, contextWindow, maxTokens, input }) => ({ provider, id, name, reasoning, contextWindow, maxTokens, input: Array.isArray(input) ? [...input] : [] }))
|
|
24
|
+
.sort((a, b) => compare(a.provider, b.provider) || compare(a.id, b.id));
|
|
25
|
+
}
|
|
26
|
+
export interface Hooks { validateDispatch(state: WorkflowState, id: number): void | Promise<void>; resetTasks(params: TodoParams): readonly number[]; userPrompt?<T>(title: string, run: () => Promise<T>): Promise<T>; summary?(): boolean; setSummary?(visible: boolean, ctx: ExtensionContext): void; msg?: Translator; ui?(): boolean; state(): WorkflowState; mutate(params: TodoParams, ctx: ExtensionContext): unknown; paint(ctx: ExtensionContext): void; protected(): boolean; canWake?(): boolean; reserveWake?(ctx: ExtensionContext): boolean; pauseAuto?(ctx: ExtensionContext): void; resumeAuto?(): boolean; beforeWake?(ctx: ExtensionContext): void; compacting?(): boolean; holdingRecovery?(): boolean; branch?(ctx: ExtensionContext): ReturnType<ExtensionContext['sessionManager']['getBranch']> }
|
|
27
|
+
const active = (job: JobSummary) => ["starting", "running", "waiting"].includes(job.status);
|
|
28
|
+
const fingerprint = (state: WorkflowState, id?: number) => {
|
|
29
|
+
const task = state.tasks.find((item) => item.id === id);
|
|
30
|
+
if (!task) return '';
|
|
31
|
+
const definition: unknown[] = [task.id, task.subject, task.description, task.blockedBy, task.metadata?.preset, task.metadata?.run, task.metadata?.step, task.metadata?.key];
|
|
32
|
+
const artifacts = contextArtifacts(task);
|
|
33
|
+
if (artifacts.length) definition.push({ artifacts }); // Preserve old signatures when no references were declared.
|
|
34
|
+
return JSON.stringify(definition);
|
|
35
|
+
};
|
|
36
|
+
/** Registration is side-effect-free; session hooks own runtime resources. */
|
|
37
|
+
/**
|
|
38
|
+
* Whether an active child blocks this todo mutation. A child that already sent an interim report
|
|
39
|
+
* may have its task completed while it keeps working on later steps; every other edit, and any
|
|
40
|
+
* task the child has not reported on, still waits for the child to stop.
|
|
41
|
+
*/
|
|
42
|
+
export function claimedBy(job: { id: string; todoId?: number }, params: TodoParams, options: { adjusted: ReadonlySet<string>; reported: boolean }): boolean {
|
|
43
|
+
if (params.action === "update") {
|
|
44
|
+
if (params.status === "completed" && options.reported && Object.keys(params).every((key) => ["action", "id", "status"].includes(key))) return false;
|
|
45
|
+
if (params.status === undefined && options.adjusted.has(job.id)) return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createAgentsOwner(pi: ExtensionAPI, hooks: Hooks) {
|
|
51
|
+
const msg = hooks.msg ?? chinese, io = agentsIO(pi, workflowNamespace.name);
|
|
52
|
+
const parentTools = io.parentTools;
|
|
53
|
+
let runtime: AgentRuntime | undefined;
|
|
54
|
+
let profiles: ProfileStore | undefined;
|
|
55
|
+
let childTools: readonly string[] = CHILD_TOOLS;
|
|
56
|
+
async function childEnvironment() {
|
|
57
|
+
// Extensions are per-profile now; the menu trusts the union so profile tool picks validate.
|
|
58
|
+
const extensions = await resolveChildExtensionPaths(await readProfileExtensionPaths(configPaths().profile), msg);
|
|
59
|
+
const tools: { name: string; description: string; from: 'core' | 'extension' }[] = [];
|
|
60
|
+
for (const tool of await parentTools()) {
|
|
61
|
+
if (!tool.delegable) continue;
|
|
62
|
+
const core = (CHILD_TOOLS as readonly string[]).includes(tool.name);
|
|
63
|
+
if (core || tool.path && extensions.includes(tool.path)) tools.push({ name: tool.name, description: tool.description, from: core ? 'core' : 'extension' });
|
|
64
|
+
}
|
|
65
|
+
childTools = tools.map(tool => tool.name);
|
|
66
|
+
return { extensions, tools };
|
|
67
|
+
}
|
|
68
|
+
let context: ExtensionContext | undefined;
|
|
69
|
+
let restoring = false;
|
|
70
|
+
let paused = false;
|
|
71
|
+
let failedRound = false;
|
|
72
|
+
let error: string | undefined;
|
|
73
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
74
|
+
let generation = 0;
|
|
75
|
+
let claiming: number | undefined;
|
|
76
|
+
const notices = new AgentNotices();
|
|
77
|
+
const journal = new AgentJournal();
|
|
78
|
+
let proposedReport: { id: string; content: string; terminalJobIds: string[] } | undefined;
|
|
79
|
+
let reportAttempts = 0;
|
|
80
|
+
const original = new Map<string, string>();
|
|
81
|
+
const adjusted = new Set<string>();
|
|
82
|
+
|
|
83
|
+
const clearDelivery = () => { if (timer) clearTimeout(timer); timer = undefined; failedRound = false; notices.clear(); proposedReport = undefined; reportAttempts = 0; };
|
|
84
|
+
const drain = () => {
|
|
85
|
+
const jobs = new Map((runtime?.inspect() ?? []).map((job) => [job.id, job]));
|
|
86
|
+
const state = hooks.state();
|
|
87
|
+
const tasks = new Map(state.tasks.map((task) => [task.id, task]));
|
|
88
|
+
const terminalJobIds = new Set<string>();
|
|
89
|
+
const content = notices.drain((notice) => {
|
|
90
|
+
const job = jobs.get(notice.jobId);
|
|
91
|
+
if (!job || job.dismissed) return;
|
|
92
|
+
if (notice.kind === 'question' && notice.requestId && !runtime?.hasRequest(job.id, notice.requestId)) return;
|
|
93
|
+
const task = job.todoId === undefined ? undefined : tasks.get(job.todoId);
|
|
94
|
+
if (job.todoId !== undefined && (!task || task.status === "deleted")) return;
|
|
95
|
+
const changedDefinition = original.has(job.id) && original.get(job.id) !== fingerprint(state, job.todoId);
|
|
96
|
+
const stale = job.taskReportStale ? msg('(历史输出,仅作参考)') : changedDefinition ? msg('(任务定义已改变,需重新核验)') : '';
|
|
97
|
+
if ((notice.kind === 'completed' || notice.kind === 'failed') && (job.status === 'completed' || job.status === 'failed') && job.reportDelivery === 'pending') terminalJobIds.add(job.id);
|
|
98
|
+
const receipt = task && ['completed', 'failed'].includes(notice.kind)
|
|
99
|
+
? task.status === 'completed' ? msg`\nTask #${task.id} 已完成,本报告仅供参考。`
|
|
100
|
+
: job.taskReportStale ? msg`\nTask #${task.id} 当前状态:${task.status};这是历史输出,不作为本轮验收依据。`
|
|
101
|
+
: msg`\nTask #${task.id} 当前状态:${task.status};核验后再更新,报告不会自动完成任务。`
|
|
102
|
+
: '';
|
|
103
|
+
// One meaningful hint instead of an empty completion notice that looks like a lost report.
|
|
104
|
+
const body = notice.kind === 'completed' && job.reportEmpty ? msg('已结束·无报告') : notice.message;
|
|
105
|
+
return msg`[${clean(job.id)}${job.todoId ? msg` / #${job.todoId}` : ""}${notice.requestId ? msg` / requestId=${clean(notice.requestId)}` : ""}] ${notice.kind}${stale}: ${body}${receipt}`;
|
|
106
|
+
}, msg);
|
|
107
|
+
return { content, terminalJobIds: [...terminalJobIds] };
|
|
108
|
+
};
|
|
109
|
+
// Boundary entries are drafts. Retain an uncommitted batch until the real request sees it.
|
|
110
|
+
const reportPersisted = (ctx: ExtensionContext) => proposedReport && ctx.sessionManager.getBranch().some((entry) => entry.type === 'custom_message' && entry.customType === AGENT_REPORT_MESSAGE && (entry.details as { deliveryId?: string } | undefined)?.deliveryId === proposedReport!.id);
|
|
111
|
+
const prepareReport = () => {
|
|
112
|
+
// A repeatedly rejected batch stays queryable in Job records; it cannot block new arrivals.
|
|
113
|
+
if (reportAttempts >= 3) { proposedReport = undefined; reportAttempts = 0; }
|
|
114
|
+
if (proposedReport) return proposedReport;
|
|
115
|
+
if (!notices.size) return undefined;
|
|
116
|
+
const batch = drain();
|
|
117
|
+
if (batch.content) proposedReport = { ...batch, content: batch.content, id: randomUUID() };
|
|
118
|
+
return proposedReport;
|
|
119
|
+
};
|
|
120
|
+
// A context filter remains free to omit persisted reports. Never force them back into a request.
|
|
121
|
+
const retirePersisted = (ctx: ExtensionContext) => { if (reportPersisted(ctx)) { proposedReport = undefined; reportAttempts = 0; } };
|
|
122
|
+
const acknowledge = (jobIds: readonly string[]) => { for (const jobId of jobIds) runtime?.markReportDelivered(jobId); };
|
|
123
|
+
const deliverIdle = () => {
|
|
124
|
+
timer = undefined;
|
|
125
|
+
if (!context || restoring || paused || failedRound || hooks.compacting?.() || hooks.state().plan || hooks.canWake?.() === false || !context.isIdle()) return;
|
|
126
|
+
retirePersisted(context);
|
|
127
|
+
const report = prepareReport();
|
|
128
|
+
if (report && reportAttempts < 3 && hooks.reserveWake?.(context) !== false) {
|
|
129
|
+
try { hooks.beforeWake?.(context); reportAttempts++; io.sendMessage({ customType: AGENT_REPORT_MESSAGE, content: report.content, details: { deliveryId: report.id }, display: true }, { triggerTurn: true, deliverAs: "followUp" }); }
|
|
130
|
+
catch (cause) { paused = true; proposedReport = undefined; reportAttempts = 0; notify(context, msg`Agent 自动唤醒失败:${String(cause)}`, "warning"); } // Durable output remains available; never retry an old wake.
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const onNotice = (notice: Notice) => {
|
|
134
|
+
if (restoring) return;
|
|
135
|
+
// A parent error wait holds reports; it must not discard them or wake before /retry.
|
|
136
|
+
if (hooks.holdingRecovery?.()) { notices.add(notice); return; }
|
|
137
|
+
if (paused || hooks.state().plan || hooks.canWake?.() === false) return;
|
|
138
|
+
notices.add(notice);
|
|
139
|
+
if (!failedRound && context?.isIdle() && !timer) timer = setTimeout(deliverIdle, 25);
|
|
140
|
+
};
|
|
141
|
+
/** Live activity refreshes only the widget; throttled and never appended to the session. */
|
|
142
|
+
let activityTimer: ReturnType<typeof setTimeout> | undefined;
|
|
143
|
+
let lastActivityStamp = 0;
|
|
144
|
+
const clearActivity = () => { if (activityTimer) clearTimeout(activityTimer); activityTimer = undefined; };
|
|
145
|
+
const onActivity = () => {
|
|
146
|
+
if (hooks.ui?.() === false || restoring || !context?.hasUI || activityTimer) return;
|
|
147
|
+
const ownGeneration = generation;
|
|
148
|
+
const refresh = () => {
|
|
149
|
+
activityTimer = undefined;
|
|
150
|
+
if (restoring || ownGeneration !== generation || !context) return;
|
|
151
|
+
lastActivityStamp = Date.now();
|
|
152
|
+
hooks.paint(context);
|
|
153
|
+
// Only elapsed tool time needs a periodic tick; thinking/output repaint on transitions.
|
|
154
|
+
if (runtime?.activities().some((item) => item.activity.kind === "tool" && item.activity.since !== undefined) && context.hasUI) activityTimer = setTimeout(refresh, 1000);
|
|
155
|
+
};
|
|
156
|
+
activityTimer = setTimeout(refresh, Math.max(0, 300 - (Date.now() - lastActivityStamp)));
|
|
157
|
+
};
|
|
158
|
+
const onChanged = () => {
|
|
159
|
+
if (restoring || !runtime || !context) return;
|
|
160
|
+
try {
|
|
161
|
+
// Append-only deltas: unchanged jobs keep their previous bytes and only the output tail is written.
|
|
162
|
+
const payload = journal.prepare(runtime.exportRecords(), runtime.nextId());
|
|
163
|
+
if (payload) { io.appendEntry(AGENTS_TYPE, payload); journal.confirm(); }
|
|
164
|
+
}
|
|
165
|
+
catch (cause) { error = msg`运行状态无法保存:${String(cause)}`; paused = true; clearDelivery(); notify(context, error, "error"); }
|
|
166
|
+
hooks.paint(context);
|
|
167
|
+
onActivity();
|
|
168
|
+
};
|
|
169
|
+
async function restore(ctx: ExtensionContext) {
|
|
170
|
+
restoring = true;
|
|
171
|
+
const ownGeneration = ++generation;
|
|
172
|
+
clearDelivery(); clearActivity();
|
|
173
|
+
original.clear(); adjusted.clear();
|
|
174
|
+
await runtime?.shutdown();
|
|
175
|
+
if (ownGeneration !== generation) return;
|
|
176
|
+
context = ctx; runtime = undefined; profiles = undefined; error = undefined;
|
|
177
|
+
// Restores never wake the model or resume processes. New explicit work re-enables delivery.
|
|
178
|
+
paused = true;
|
|
179
|
+
try {
|
|
180
|
+
const loadedProfiles = new ProfileStore({ path: configPaths().profile, registry: ctx.modelRegistry, getTools: () => childTools });
|
|
181
|
+
// Profiles are loaded only when selected or queried; snapshots do not depend on that file.
|
|
182
|
+
if (ownGeneration !== generation) return;
|
|
183
|
+
profiles = loadedProfiles;
|
|
184
|
+
const provider = pi.getFlag("dag-workflow-test-child-provider");
|
|
185
|
+
const loadedRuntime = new AgentRuntime({ cwd: ctx.cwd, profiles, getInheritedModel: () => context?.model ? { provider: context.model.provider, id: context.model.id } : undefined,
|
|
186
|
+
getInheritedThinking: () => pi.getThinkingLevel?.() ?? 'off', getInheritedTools: () => pi.getActiveTools?.() ?? CORE_TOOLS,
|
|
187
|
+
getChildExtensions: async (profile: { childExtensions?: string[] }) => resolveChildExtensionPaths(profile.childExtensions, msg),
|
|
188
|
+
onChanged: () => { if (runtime === loadedRuntime) onChanged(); },
|
|
189
|
+
onNotice: (notice) => { if (runtime === loadedRuntime) onNotice(notice); },
|
|
190
|
+
onActivity: () => { if (runtime === loadedRuntime) onActivity(); },
|
|
191
|
+
...(typeof provider === "string" && provider ? { testExtensions: [provider] } : {
|
|
192
|
+
getModelBootstrap: async (ref) => {
|
|
193
|
+
const model = context?.modelRegistry.find(ref.provider, ref.id);
|
|
194
|
+
if (!model || !context) throw new Error(msg`找不到模型 ${ref.provider}/${ref.id}`);
|
|
195
|
+
const auth = await context.modelRegistry.getApiKeyAndHeaders(model);
|
|
196
|
+
if (!auth.ok) throw new Error(auth.error);
|
|
197
|
+
return { model: { ...model, baseUrl: auth.baseUrl ?? model.baseUrl, headers: { ...model.headers, ...auth.headers } }, ...(auth.apiKey ? { apiKey: auth.apiKey } : {}), ...(auth.env ? { env: auth.env } : {}) };
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
runtime = loadedRuntime;
|
|
202
|
+
// Fold the active branch once: version-1 snapshots replace, version-2 deltas continue.
|
|
203
|
+
const folded = foldAgentEntries(hooks.branch?.(ctx) ?? ctx.sessionManager.getBranch());
|
|
204
|
+
await loadedRuntime.importSummaries(folded.records, folded.nextId);
|
|
205
|
+
if (ownGeneration !== generation) return;
|
|
206
|
+
// The confirmed base is what was actually saved, before import normalizes metadata
|
|
207
|
+
// and interrupts old live jobs. Persist those changes on the next real update.
|
|
208
|
+
journal.hydrate(folded.records, runtime.nextId());
|
|
209
|
+
} catch (cause) { if (ownGeneration === generation) { error = String(cause); notify(ctx, msg`Agent 恢复/配置失败:${error};/agents reset 可明确清除运行记录`, "error"); } }
|
|
210
|
+
finally { if (ownGeneration === generation) { restoring = false; hooks.paint(ctx); } }
|
|
211
|
+
}
|
|
212
|
+
pi.on("session_start", (_event, ctx) => restore(ctx));
|
|
213
|
+
pi.on("session_tree", (_event, ctx) => restore(ctx));
|
|
214
|
+
// Stop before switching the active branch, so writes cannot escape into the new branch.
|
|
215
|
+
// A cancelled navigation leaves the old session live: undo the stop so tools keep working.
|
|
216
|
+
const stopBeforeNavigation = async () => {
|
|
217
|
+
const hadActive = runtime?.activeCount() ?? 0;
|
|
218
|
+
restoring = true; generation++; paused = true; clearDelivery(); clearActivity();
|
|
219
|
+
try {
|
|
220
|
+
await runtime?.shutdown();
|
|
221
|
+
// A later extension may cancel navigation: retain an available, interrupted runtime.
|
|
222
|
+
if (runtime) await runtime.importSummaries(runtime.exportRecords());
|
|
223
|
+
} finally { restoring = false; }
|
|
224
|
+
if (hadActive) onChanged();
|
|
225
|
+
};
|
|
226
|
+
pi.on("session_before_switch", stopBeforeNavigation);
|
|
227
|
+
pi.on("session_before_fork", stopBeforeNavigation);
|
|
228
|
+
pi.on("session_before_tree", stopBeforeNavigation);
|
|
229
|
+
pi.on("session_shutdown", async () => { restoring = true; generation++; clearDelivery(); clearActivity(); await runtime?.shutdown(); runtime = undefined; context = undefined; });
|
|
230
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
231
|
+
context = ctx;
|
|
232
|
+
(event.systemPromptOptions.promptGuidelines ??= []).push(
|
|
233
|
+
'Delegate only independent, verifiable work to one-tier children; no grandchildren. Provide inputs, scope and expected evidence.',
|
|
234
|
+
'Discover child tools with subagent_inspect tools:true; configured extension tools require an explicit tools list. Tool selection is not a sandbox or additional user authorization.',
|
|
235
|
+
"The parent decides tool changes under existing user authorization. A child's request is not authorization; inspect interrupted work and external effects before resuming.",
|
|
236
|
+
'If the assignment serves an existing unblocked Todo, pass todoId; otherwise omit it. Do not create a Todo merely to delegate. Keep serial steps with one child and parallelize independent work.',
|
|
237
|
+
'Keep the critical path and final verification in the parent. After a child fails or is interrupted, inspect diagnostics and external side effects and prefer resumeFrom on the same Todo; do not create a replacement Todo or blindly replay work. A user-stopped objective needs explicit authorization to continue.',
|
|
238
|
+
'Verify reports and artifacts before completing a Todo. Never guess profiles or models.',
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
pi.on("input", (event, ctx) => { context = ctx; if (event.source !== "extension" && !hooks.state().plan && !event.text.trim().startsWith("/")) { paused = false; failedRound = false; } });
|
|
242
|
+
const reportBoundary = (outcome: string, ctx: ExtensionContext, entries: SessionBoundaryDraft[] = []) => {
|
|
243
|
+
// A model error may recover natively or through Goal. Hold reports for the successful turn
|
|
244
|
+
// instead of dropping them or racing a separate idle wake. Explicit aborts still pause.
|
|
245
|
+
if (outcome === "error") { failedRound = true; return; }
|
|
246
|
+
if (outcome !== "completed") { paused = true; clearDelivery(); return; }
|
|
247
|
+
failedRound = false;
|
|
248
|
+
if (restoring || paused || hooks.compacting?.() || hooks.state().plan || hooks.canWake?.() === false) return;
|
|
249
|
+
retirePersisted(ctx);
|
|
250
|
+
const report = prepareReport();
|
|
251
|
+
if (report && reportAttempts < 3 && hooks.reserveWake?.(ctx) !== false) {
|
|
252
|
+
reportAttempts++;
|
|
253
|
+
return { entries: [...entries, { type: "custom_message" as const, customType: AGENT_REPORT_MESSAGE, content: report.content, details: { deliveryId: report.id }, display: true }], continue: true };
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
pi.on("turn_end", (event, ctx) => reportBoundary(event.outcome, ctx, event.entries));
|
|
257
|
+
pi.on("agent_before_settle", (event, ctx) => reportBoundary(event.outcome, ctx, event.entries));
|
|
258
|
+
pi.on('context', (event) => {
|
|
259
|
+
if (!proposedReport) return;
|
|
260
|
+
if (event.messages.some((message) => (message as { details?: { deliveryId?: string } }).details?.deliveryId === proposedReport!.id)) {
|
|
261
|
+
acknowledge(proposedReport.terminalJobIds); proposedReport = undefined; reportAttempts = 0;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
pi.on('session_compact', (_event, ctx) => { context = ctx; if (!paused && !timer) timer = setTimeout(deliverIdle, 25); });
|
|
265
|
+
pi.on("agent_settled", (_event, ctx) => { context = ctx; if (!paused && !failedRound && !timer) timer = setTimeout(deliverIdle, 25); });
|
|
266
|
+
|
|
267
|
+
function ready(ctx: ExtensionContext, execution = true): AgentRuntime {
|
|
268
|
+
context = ctx;
|
|
269
|
+
if (restoring || !runtime || error || hooks.protected()) throw new Error(error ?? msg("工作流正在恢复或状态受保护"));
|
|
270
|
+
if (execution && hooks.state().plan) throw new Error(msg("Plan 中不实施/派发/发送执行信息;先 /plan off"));
|
|
271
|
+
return runtime;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Display-only projection for tool results: the linked Todo's current state, never persisted. */
|
|
275
|
+
const todoStatusOf = (todoId?: number) => {
|
|
276
|
+
if (todoId === undefined) return undefined;
|
|
277
|
+
const task = hooks.state().tasks.find((item) => item.id === todoId);
|
|
278
|
+
return task && task.status !== "deleted" ? task.status : undefined;
|
|
279
|
+
};
|
|
280
|
+
const reply = (data: unknown) => ({ content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data });
|
|
281
|
+
return {
|
|
282
|
+
ready, childEnvironment, parentTools, todoStatusOf, availableModels,
|
|
283
|
+
profiles: () => profiles, runtime: () => runtime, generation: () => generation, paused: () => paused,
|
|
284
|
+
dropNotice: (jobId: string, version?: number) => notices.drop(jobId, version),
|
|
285
|
+
adjusted: (jobId: string) => adjusted.add(jobId),
|
|
286
|
+
async reset(ctx: ExtensionContext, ownGeneration: number) {
|
|
287
|
+
restoring = true; clearDelivery(); clearActivity(); await runtime?.shutdown();
|
|
288
|
+
if (ownGeneration !== generation) return;
|
|
289
|
+
io.appendEntry(AGENTS_TYPE, { version: 1, jobs: [], nextId: runtime?.nextId() ?? 1 });
|
|
290
|
+
journal.reset();
|
|
291
|
+
await restore(ctx);
|
|
292
|
+
},
|
|
293
|
+
async spawn(params: AgentSpawnParams, signal: AbortSignal | undefined, ctx: ExtensionContext) {
|
|
294
|
+
if (signal?.aborted) throw new Error('Dispatch aborted');
|
|
295
|
+
const agent = ready(ctx);
|
|
296
|
+
if (params.profile !== undefined && params.profile !== 'inherit') { await childEnvironment(); await profiles!.load(); }
|
|
297
|
+
if (signal?.aborted) throw new Error('Dispatch aborted');
|
|
298
|
+
const before = hooks.state();
|
|
299
|
+
const previous = params.resumeFrom === undefined ? undefined : agent.inspect(params.resumeFrom)[0];
|
|
300
|
+
const todoId = params.todoId ?? previous?.todoId;
|
|
301
|
+
const task = before.tasks.find((item) => item.id === todoId);
|
|
302
|
+
if (todoId !== undefined) {
|
|
303
|
+
if (!task || task.status === "completed" || task.status === "deleted") throw new Error(msg("只能派发未完成的当前 Task"));
|
|
304
|
+
await hooks.validateDispatch(before, task.id); // Validate without claiming work on startup failure.
|
|
305
|
+
}
|
|
306
|
+
// Opt-in: the parent decides per dispatch, and only a bound task can carry a fragment brief.
|
|
307
|
+
const context = params.context === true && task ? taskContext(task.id, before.tasks, agent.exportRecords()) : undefined;
|
|
308
|
+
const ownGeneration = generation;
|
|
309
|
+
paused = false; failedRound = false;
|
|
310
|
+
// Refresh the trusted-tool union from the profile file so validation matches this dispatch.
|
|
311
|
+
await childEnvironment();
|
|
312
|
+
const job = await agent.spawn({
|
|
313
|
+
task: params.task,
|
|
314
|
+
...(todoId === undefined ? {} : { todoId, taskDefinition: createHash('sha256').update(fingerprint(before, todoId)).digest('hex') }),
|
|
315
|
+
...(params.resumeFrom === undefined ? {} : { resumeFrom: params.resumeFrom }),
|
|
316
|
+
...(params.profile === undefined ? {} : { profile: params.profile }),
|
|
317
|
+
...(params.model === undefined ? {} : { model: params.model }),
|
|
318
|
+
...(params.thinking === undefined ? {} : { thinking: params.thinking }),
|
|
319
|
+
...(params.tools === undefined ? {} : { tools: params.tools }),
|
|
320
|
+
...(params.skills === undefined ? {} : { skills: params.skills }),
|
|
321
|
+
...(params.timeout === undefined ? {} : { timeout: params.timeout }),
|
|
322
|
+
...(context ? { context } : {}),
|
|
323
|
+
...(signal ? { signal } : {}),
|
|
324
|
+
});
|
|
325
|
+
if (signal?.aborted) { await agent.cancel(job.id); throw new Error('Dispatch aborted; child stopped'); }
|
|
326
|
+
if (ownGeneration !== generation) throw new Error(msg("会话已切换,派发已停止"));
|
|
327
|
+
original.set(job.id, fingerprint(before, todoId));
|
|
328
|
+
if (task && !['failed', 'cancelled', 'interrupted'].includes(job.status)) {
|
|
329
|
+
claiming = task.id;
|
|
330
|
+
try { hooks.mutate({ action: "update", id: task.id, status: "in_progress" }, ctx); }
|
|
331
|
+
finally { claiming = undefined; }
|
|
332
|
+
}
|
|
333
|
+
const ignored = job.ignoredInheritedTools ?? [];
|
|
334
|
+
const result = reply({ jobId: job.id, status: job.status, model: job.model, thinking: job.thinking, tools: job.tools, skills: job.skills, ...(ignored.length ? { ignoredInheritedTools: ignored } : {}), ...(task ? { todoId: task.id, todoStatus: hooks.state().tasks.find((item) => item.id === task.id)?.status } : {}), ...(job.error ? { error: job.error } : {}), ...(job.failure ? { failure: job.failure } : {}), ...(job.resumeFrom ? { resumeFrom: job.resumeFrom } : {}) });
|
|
335
|
+
return ['failed', 'cancelled', 'interrupted'].includes(job.status) ? { ...result, isError: true } : result;
|
|
336
|
+
|
|
337
|
+
},
|
|
338
|
+
async inspect(params: AgentInspectParams, signal: AbortSignal | undefined, ctx: ExtensionContext) {
|
|
339
|
+
if (params.tools === true) {
|
|
340
|
+
if ([params.models, params.jobId, params.profile, params.profiles, params.output, params.timeout, params.until, params.after, params.query, params.limit, params.offset].some(value => value !== undefined)) throw new Error(msg('tools:true 仅用于工具目录查询,不能与模型、Job、Profile 或输出参数组合'));
|
|
341
|
+
return reply({ tools: (await childEnvironment()).tools });
|
|
342
|
+
}
|
|
343
|
+
if (params.models !== true && (params.query !== undefined || params.limit !== undefined || params.offset !== undefined)) throw new Error(msg('模型搜索与分页参数仅用于 models:true'));
|
|
344
|
+
if (params.models === true) {
|
|
345
|
+
if ([params.jobId, params.profile, params.profiles, params.output, params.timeout, params.until, params.after].some((value) => value !== undefined)) throw new Error(msg('models:true 不能与 Job、Profile、输出或等待参数组合'));
|
|
346
|
+
const query = params.query?.trim().toLowerCase() ?? '';
|
|
347
|
+
const models = availableModels(ctx).filter((model) => !query || [model.provider, model.id, model.name].some((value) => value.toLowerCase().includes(query)));
|
|
348
|
+
const offset = params.offset ?? 0, limit = params.limit ?? 20;
|
|
349
|
+
const page = models.slice(offset, offset + limit);
|
|
350
|
+
return reply({ models: page, total: models.length, nextOffset: offset + page.length < models.length ? offset + page.length : null });
|
|
351
|
+
}
|
|
352
|
+
if (params.output === true && (!params.jobId || params.profile !== undefined || params.profiles === true || params.profiles === 'summary')) throw new Error(msg('output:true 需要 jobId,不能与 Profile 查询组合'));
|
|
353
|
+
if (params.output !== true && (params.timeout !== undefined || params.until !== undefined || params.after !== undefined)) throw new Error(msg('等待参数仅用于 output:true;普通状态查询不等待'));
|
|
354
|
+
if (params.profile !== undefined && (params.profiles === true || params.profiles === 'summary')) throw new Error(msg('profile 与 profiles 不能同时指定:profile 返回单个完整配置,profiles 返回列表摘要或全量'));
|
|
355
|
+
const agent = ready(ctx, false);
|
|
356
|
+
if (params.output === true) {
|
|
357
|
+
const result = await agent.wait(params.jobId!, { ...(params.timeout === undefined ? {} : { timeout: params.timeout }), ...(signal ? { signal } : {}), ...(params.until === undefined ? {} : { until: params.until }), ...(params.after === undefined ? {} : { after: params.after }) });
|
|
358
|
+
notices.drop(params.jobId!, result.reportVersion);
|
|
359
|
+
if (['completed', 'failed'].includes(result.status) && result.reportDelivery === 'pending' && agent.markReportDelivered(params.jobId!)) result.reportDelivery = 'delivered';
|
|
360
|
+
const todoStatus = todoStatusOf(result.todoId);
|
|
361
|
+
return reply(todoStatus === undefined ? result : { ...result, todoStatus });
|
|
362
|
+
}
|
|
363
|
+
const directoryOnly = params.jobId === undefined && (params.profile !== undefined || params.profiles === 'summary');
|
|
364
|
+
const jobs = directoryOnly ? undefined : agent.inspect(params.jobId).map((job) => { const todoStatus = todoStatusOf(job.todoId); return todoStatus === undefined ? job : { ...job, todoStatus }; });
|
|
365
|
+
if (params.profile !== undefined || params.profiles === true || params.profiles === 'summary') { await childEnvironment(); await profiles!.load(); }
|
|
366
|
+
const listed = params.profile !== undefined || params.profiles === true || params.profiles === 'summary' ? profiles!.list() : [];
|
|
367
|
+
let payload: Record<string, unknown> = {};
|
|
368
|
+
if (params.profile !== undefined) {
|
|
369
|
+
const found = listed.find((item) => item.name === params.profile);
|
|
370
|
+
if (!found) throw new Error(msg`未找到 Profile ${params.profile}`);
|
|
371
|
+
payload = { profile: found };
|
|
372
|
+
} else if (params.profiles === true) payload = { profiles: listed, profilePath: configPaths().profile };
|
|
373
|
+
else if (params.profiles === 'summary') payload = { profiles: listed.map(({ instructions: _omitted, ...profile }) => profile) };
|
|
374
|
+
return reply({ ...(jobs === undefined ? {} : { jobs, paused }), ...payload });
|
|
375
|
+
|
|
376
|
+
},
|
|
377
|
+
async send(params: AgentSendParams, signal: AbortSignal | undefined, ctx: ExtensionContext) {
|
|
378
|
+
const agent = ready(ctx);
|
|
379
|
+
if (params.tools !== undefined) await childEnvironment();
|
|
380
|
+
if (!!params.recipient === !!params.requestId) throw new Error(msg("recipient 与 requestId 需且只能提供一个"));
|
|
381
|
+
if (params.tools !== undefined && (!params.recipient || params.requestId !== undefined || params.interrupt !== true)) throw new Error(msg('调整 tools 需要 recipient 与 interrupt:true,不能使用 requestId'));
|
|
382
|
+
const previousVersion = params.interrupt && params.recipient ? agent.inspect(params.recipient)[0]!.reportVersion : undefined;
|
|
383
|
+
const result = await agent.send({ ...params, ...(signal ? { signal } : {}) });
|
|
384
|
+
if (params.recipient) {
|
|
385
|
+
adjusted.add(params.recipient);
|
|
386
|
+
if (params.interrupt) notices.drop(params.recipient, previousVersion);
|
|
387
|
+
}
|
|
388
|
+
return reply(result);
|
|
389
|
+
|
|
390
|
+
},
|
|
391
|
+
async cancel(params: AgentCancelParams, _signal: AbortSignal | undefined, ctx: ExtensionContext) { const agent = ready(ctx, false); const job = agent.inspect(params.jobId)[0]!; if (params.remove || job.status !== 'completed' || job.reportDelivery !== 'pending') notices.drop(params.jobId); await agent.cancel(params.jobId, { ...(params.remove !== undefined ? { remove: params.remove } : {}) }); original.delete(params.jobId); adjusted.delete(params.jobId); return reply({ jobId: params.jobId, stopped: true, removed: params.remove ?? false });
|
|
392
|
+
},
|
|
393
|
+
pauseAutomatic() { paused = true; clearDelivery(); },
|
|
394
|
+
resumeAutomatic() { paused = false; failedRound = false; },
|
|
395
|
+
summaries: () => runtime?.viewSummaries() ?? [],
|
|
396
|
+
cleanupProtection(tasks: readonly Todo[]) {
|
|
397
|
+
if (restoring || error) throw new Error(error ?? msg('Agent 状态正在恢复'));
|
|
398
|
+
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
399
|
+
const protectedIds = new Set<number>();
|
|
400
|
+
for (const job of runtime?.inspect() ?? []) {
|
|
401
|
+
if (job.todoId === undefined) continue;
|
|
402
|
+
const task = byId.get(job.todoId);
|
|
403
|
+
const verification = job.status === 'completed' && !job.taskReportStale && task && ['pending', 'in_progress'].includes(task.status);
|
|
404
|
+
if (active(job) || job.reportDelivery === 'pending' || verification) protectedIds.add(job.todoId);
|
|
405
|
+
}
|
|
406
|
+
return protectedIds;
|
|
407
|
+
},
|
|
408
|
+
cleanupRecords(scope: ClearScope, before: readonly Todo[], remaining: readonly Todo[]) {
|
|
409
|
+
if (restoring || error) throw new Error(error ?? msg('Agent 状态正在恢复'));
|
|
410
|
+
const beforeById = new Map(before.map((task) => [task.id, task]));
|
|
411
|
+
const remainingById = new Map(remaining.map((task) => [task.id, task]));
|
|
412
|
+
const ids = (runtime?.inspect() ?? []).filter((job) => {
|
|
413
|
+
if (active(job) || job.reportDelivery === 'pending') return false;
|
|
414
|
+
const task = job.todoId === undefined ? undefined : remainingById.get(job.todoId);
|
|
415
|
+
if (task && ['pending', 'in_progress'].includes(task.status)) return false;
|
|
416
|
+
if (scope !== 'completed') return true;
|
|
417
|
+
return job.status === 'completed' || job.todoId !== undefined && beforeById.get(job.todoId)?.status === 'completed';
|
|
418
|
+
}).map((job) => job.id);
|
|
419
|
+
for (const id of ids) { notices.drop(id); original.delete(id); adjusted.delete(id); }
|
|
420
|
+
return runtime?.prune(ids) ?? 0;
|
|
421
|
+
},
|
|
422
|
+
assertPlanEntry() { if (restoring || error) throw new Error(error ?? msg("Agent 状态正在恢复")); if (runtime?.activeCount()) throw new Error(msg("子 Agent 仍在执行/等待;请先等待结束或明确取消,再进入 Plan")); clearDelivery(); paused = true; },
|
|
423
|
+
assertTodoMutation(params: TodoParams) {
|
|
424
|
+
if (params.action === "list" || params.action === "get" || params.action === 'presets' || params.action === "create" || claiming !== undefined && params.action === "update" && params.id === claiming) return;
|
|
425
|
+
if (!runtime?.activeCount()) return;
|
|
426
|
+
const resetIds = params.action === "reset" ? new Set(hooks.resetTasks(params)) : undefined;
|
|
427
|
+
const jobs = (runtime?.inspect() ?? []).filter(active).filter((job) => params.action === "clear" || job.todoId !== undefined && (resetIds ? resetIds.has(job.todoId) : job.todoId === params.id));
|
|
428
|
+
for (const job of jobs) {
|
|
429
|
+
const reported = (runtime?.reports(job.id) ?? 0) > 0;
|
|
430
|
+
if (claimedBy(job, params, { adjusted, reported })) throw new Error(msg`任务 #${job.todoId} 关联活动 ${job.id};内容修改先发送明确调整信息;未收到它的汇报前不能完成,删除/清空请先停止该 Agent`);
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
afterTodoMutation(params: TodoParams, before?: WorkflowState) {
|
|
434
|
+
if (params.action === 'update' && params.id !== undefined && (params.subject !== undefined || params.description !== undefined || params.metadata !== undefined || Boolean(params.addBlockedBy?.length || params.removeBlockedBy?.length))) {
|
|
435
|
+
const definition = fingerprint(hooks.state(), params.id);
|
|
436
|
+
const changed = before ? fingerprint(before, params.id) !== definition : undefined;
|
|
437
|
+
for (const job of runtime?.inspect() ?? []) if (job.todoId === params.id && active(job) && (changed ?? original.get(job.id) !== definition)) {
|
|
438
|
+
runtime?.invalidateReports(job.id); notices.drop(job.id);
|
|
439
|
+
}
|
|
440
|
+
if (changed) runtime?.invalidateTaskReports([params.id]);
|
|
441
|
+
}
|
|
442
|
+
if (params.action === 'reset') runtime?.invalidateTaskReports(hooks.resetTasks(params));
|
|
443
|
+
else if (params.action === 'update' && ['pending', 'in_progress'].includes(params.status ?? '') && params.id !== undefined) runtime?.invalidateTaskReports([params.id], true);
|
|
444
|
+
if (adjusted.size === 0) return;
|
|
445
|
+
for (const job of runtime?.inspect() ?? []) if (job.todoId === params.id) adjusted.delete(job.id);
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|