@evandu/super-pi 0.1.1 → 0.1.3

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.
Files changed (38) hide show
  1. package/agents/super-pi-task/core.ts +748 -485
  2. package/agents/super-pi-task/index.ts +198 -141
  3. package/dist/client/assets/{index-CsbZzuMy.css → index-BhFeQ1E-.css} +1 -1
  4. package/dist/client/assets/index-D_VwFuME.js +43 -0
  5. package/dist/client/index.html +2 -2
  6. package/dist/server/agents.d.ts +20 -1
  7. package/dist/server/agents.js +92 -18
  8. package/dist/server/application/catalog-service.d.ts +22 -2
  9. package/dist/server/application/catalog-service.js +61 -9
  10. package/dist/server/application/conversation-service.d.ts +3 -2
  11. package/dist/server/application/conversation-service.js +5 -1
  12. package/dist/server/application/run-service.d.ts +49 -1
  13. package/dist/server/application/run-service.js +81 -2
  14. package/dist/server/application/workspace-service.d.ts +3 -2
  15. package/dist/server/application/workspace-service.js +5 -1
  16. package/dist/server/capability.d.ts +89 -0
  17. package/dist/server/capability.js +136 -0
  18. package/dist/server/command-handler.js +12 -0
  19. package/dist/server/http-server.js +20 -0
  20. package/dist/server/index.js +47 -9
  21. package/dist/server/runtime.d.ts +6 -2
  22. package/dist/server/runtime.js +19 -4
  23. package/dist/server/server-context.d.ts +1 -0
  24. package/dist/server/store.d.ts +48 -0
  25. package/dist/server/store.js +160 -1
  26. package/dist/server/subagent-runs.d.ts +2 -0
  27. package/dist/server/subagent-runs.js +4 -0
  28. package/dist/server/subagents.d.ts +3 -2
  29. package/dist/server/subagents.js +5 -1
  30. package/dist/server/task-tool.d.ts +21 -0
  31. package/dist/server/task-tool.js +21 -0
  32. package/dist/server/types.d.ts +45 -0
  33. package/dist/server/workflow-engine.d.ts +129 -0
  34. package/dist/server/workflow-engine.js +253 -0
  35. package/dist/server/workflow.d.ts +287 -0
  36. package/dist/server/workflow.js +387 -0
  37. package/package.json +8 -2
  38. package/dist/client/assets/index-BMQsEdio.js +0 -24
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#101828" />
7
7
  <title>Super Pi</title>
8
- <script type="module" crossorigin src="/assets/index-BMQsEdio.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-CsbZzuMy.css">
8
+ <script type="module" crossorigin src="/assets/index-D_VwFuME.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BhFeQ1E-.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -146,7 +146,26 @@ export type AgentModeFilter = "primary" | "subagent";
146
146
  * so the split is a filter on the parsed field, never a second directory or a second parser. A caller
147
147
  * with no argument gets exactly what it always got.
148
148
  */
149
- export declare function readAgents(directory: string, options?: {
149
+ /**
150
+ * Expands a leading tilde (`~` or `~/...` or `~\...`) to the user's home directory.
151
+ */
152
+ export declare function expandTilde(filepath: string): string;
153
+ export interface ResolveAgentDirectoriesOptions {
154
+ customDirs?: readonly string[];
155
+ env?: NodeJS.ProcessEnv;
156
+ rootDir?: string;
157
+ }
158
+ /**
159
+ * Resolves the ordered list of directories to discover agents from:
160
+ * 1. Custom directories configured by the user in app settings (or options)
161
+ * 2. SUPER_PI_AGENTS_DIR environment variable if set
162
+ * 3. User global agent directories (~/.super_pi/agents, ~/.super-pi/agents, ~/.pi/agents)
163
+ * 4. Built-in repository/package agents directory (<rootDir>/agents)
164
+ *
165
+ * Duplicate paths are preserved in order of first appearance.
166
+ */
167
+ export declare function resolveAgentDirectories(options?: ResolveAgentDirectoriesOptions): string[];
168
+ export declare function readAgents(directory: string | readonly string[], options?: {
150
169
  mode?: AgentModeFilter;
151
170
  }): Promise<AgentScan>;
152
171
  /** Parses one definition file, or returns the reason it is not a usable agent. */
@@ -1,5 +1,6 @@
1
1
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
3
4
  /**
4
5
  * A guard rather than a cast: it is the only thing that lets `tools:` be checked once and then used as
5
6
  * a map everywhere without the compiler complaining. It narrows to the index-signature shape that
@@ -332,32 +333,105 @@ export function parseAgentModel(model) {
332
333
  * so the split is a filter on the parsed field, never a second directory or a second parser. A caller
333
334
  * with no argument gets exactly what it always got.
334
335
  */
335
- export async function readAgents(directory, options = {}) {
336
- const mode = options.mode ?? "primary";
337
- let entries;
338
- try {
339
- entries = await readdir(directory);
336
+ /**
337
+ * Expands a leading tilde (`~` or `~/...` or `~\...`) to the user's home directory.
338
+ */
339
+ export function expandTilde(filepath) {
340
+ if (filepath === "~")
341
+ return homedir();
342
+ if (filepath.startsWith("~/") || filepath.startsWith("~\\")) {
343
+ return join(homedir(), filepath.slice(2));
340
344
  }
341
- catch (error) {
342
- if (error.code === "ENOENT")
343
- return { agents: [], problems: [] };
344
- throw error;
345
+ return filepath;
346
+ }
347
+ /**
348
+ * Resolves the ordered list of directories to discover agents from:
349
+ * 1. Custom directories configured by the user in app settings (or options)
350
+ * 2. SUPER_PI_AGENTS_DIR environment variable if set
351
+ * 3. User global agent directories (~/.super_pi/agents, ~/.super-pi/agents, ~/.pi/agents)
352
+ * 4. Built-in repository/package agents directory (<rootDir>/agents)
353
+ *
354
+ * Duplicate paths are preserved in order of first appearance.
355
+ */
356
+ export function resolveAgentDirectories(options = {}) {
357
+ const dirs = [];
358
+ const add = (dir) => {
359
+ if (!dir)
360
+ return;
361
+ const trimmed = dir.trim();
362
+ if (!trimmed)
363
+ return;
364
+ const expanded = expandTilde(trimmed);
365
+ const resolved = resolve(expanded);
366
+ if (!dirs.includes(resolved)) {
367
+ dirs.push(resolved);
368
+ }
369
+ };
370
+ if (options.customDirs) {
371
+ for (const d of options.customDirs)
372
+ add(d);
345
373
  }
374
+ const env = options.env ?? process.env;
375
+ if (env.SUPER_PI_AGENTS_DIR) {
376
+ add(env.SUPER_PI_AGENTS_DIR);
377
+ }
378
+ else {
379
+ try {
380
+ const home = homedir();
381
+ add(join(home, ".super_pi", "agents"));
382
+ add(join(home, ".super-pi", "agents"));
383
+ add(join(home, ".pi", "agents"));
384
+ }
385
+ catch {
386
+ // homedir() might throw in restricted sandbox environments
387
+ }
388
+ const root = options.rootDir ?? resolve(import.meta.dirname, "../..");
389
+ add(join(root, "agents"));
390
+ }
391
+ return dirs;
392
+ }
393
+ export async function readAgents(directory, options = {}) {
394
+ const mode = options.mode ?? "primary";
395
+ const rawDirs = Array.isArray(directory) ? directory : [directory];
396
+ const dirs = rawDirs.filter(Boolean);
346
397
  const agents = [];
347
398
  const problems = [];
348
- for (const file of entries.filter((name) => name.toLowerCase().endsWith(".md")).sort()) {
349
- const source = await readFile(join(directory, file), "utf8");
350
- const parsed = parseDefinition(file, source);
351
- if ("message" in parsed)
352
- problems.push({ file, message: parsed.message });
353
- else
354
- agents.push(parsed);
399
+ const seenIds = new Set();
400
+ for (const dir of dirs) {
401
+ let entries;
402
+ try {
403
+ entries = await readdir(dir);
404
+ }
405
+ catch (error) {
406
+ if (error.code === "ENOENT")
407
+ continue;
408
+ throw error;
409
+ }
410
+ for (const file of entries.filter((name) => name.toLowerCase().endsWith(".md")).sort()) {
411
+ const source = await readFile(join(dir, file), "utf8");
412
+ const parsed = parseDefinition(file, source);
413
+ if ("message" in parsed) {
414
+ const fileId = file.replace(/\.md$/i, "");
415
+ if (!seenIds.has(fileId)) {
416
+ problems.push({ file, message: parsed.message });
417
+ }
418
+ }
419
+ else {
420
+ if (!seenIds.has(parsed.id)) {
421
+ seenIds.add(parsed.id);
422
+ agents.push({ ...parsed, sourceDir: dir });
423
+ }
424
+ }
425
+ }
355
426
  }
356
427
  // The half that was NOT asked for is filtered out here rather than marked in the UI: sub-agents are
357
428
  // invoked BY an agent and never chosen by a human, so listing one in the picker would promise a role
358
429
  // the runtime cannot honour. The remainder is alphabetical by id, so either roster reads as a list
359
430
  // rather than as a directory listing.
360
- return { agents: agents.filter((agent) => agent.mode === mode).sort((a, b) => a.id.localeCompare(b.id)), problems };
431
+ return {
432
+ agents: agents.filter((agent) => agent.mode === mode).sort((a, b) => a.id.localeCompare(b.id)),
433
+ problems,
434
+ };
361
435
  }
362
436
  /** Only `#rgb` and `#rrggbb`. A `color:` frontmatter value is rendered as a CSS background, so it has to
363
437
  * be a colour and nothing else - accepting an arbitrary string would put author-controlled text into a
@@ -1,14 +1,34 @@
1
1
  import type { AgentRuntime } from "../runtime.js";
2
2
  import type { AgentCatalog, ModelCatalog, ModelSelection } from "../types.js";
3
3
  import type { EventBroadcaster } from "../server-context.js";
4
+ import type { Store } from "../store.js";
4
5
  /**
5
6
  * Service managing model catalogs and agent definitions (primary and subagents).
6
7
  */
7
8
  export declare class CatalogService {
8
9
  private readonly runtime;
9
10
  private readonly broadcaster;
10
- private readonly agentsDir;
11
- constructor(runtime: AgentRuntime, broadcaster: EventBroadcaster, agentsDir: string);
11
+ private readonly store?;
12
+ private readonly rootDir?;
13
+ private readonly onDirsChanged?;
14
+ private agentDirs;
15
+ constructor(runtime: AgentRuntime, broadcaster: EventBroadcaster, agentsDir: string | readonly string[], store?: Store | undefined, rootDir?: string | undefined, onDirsChanged?: ((dirs: string[]) => void) | undefined);
16
+ /**
17
+ * Returns current active agent discovery directories.
18
+ */
19
+ getAgentDirs(): string[];
20
+ /**
21
+ * Updates agent discovery directories and notifies subscribers.
22
+ */
23
+ setAgentDirs(dirs: string[]): void;
24
+ /**
25
+ * Updates user-configured custom agent directories in store and triggers rescan.
26
+ */
27
+ setCustomDirs(dirs: string[]): Promise<void>;
28
+ /**
29
+ * Immediately re-reads agent definitions from disk and broadcasts catalogs.
30
+ */
31
+ rescan(): Promise<void>;
12
32
  /**
13
33
  * Fetches the model catalog from runtime, optionally tailored to a selected model.
14
34
  */
@@ -1,4 +1,4 @@
1
- import { readAgents } from "../agents.js";
1
+ import { readAgents, resolveAgentDirectories } from "../agents.js";
2
2
  function summarize(agent) {
3
3
  const { prompt, toolFlags, ...summary } = agent;
4
4
  const toolSummary = toolFlags.includes("--no-tools")
@@ -16,11 +16,54 @@ function summarize(agent) {
16
16
  export class CatalogService {
17
17
  runtime;
18
18
  broadcaster;
19
- agentsDir;
20
- constructor(runtime, broadcaster, agentsDir) {
19
+ store;
20
+ rootDir;
21
+ onDirsChanged;
22
+ agentDirs;
23
+ constructor(runtime, broadcaster, agentsDir, store, rootDir, onDirsChanged) {
21
24
  this.runtime = runtime;
22
25
  this.broadcaster = broadcaster;
23
- this.agentsDir = agentsDir;
26
+ this.store = store;
27
+ this.rootDir = rootDir;
28
+ this.onDirsChanged = onDirsChanged;
29
+ this.agentDirs = Array.isArray(agentsDir) ? [...agentsDir] : [agentsDir];
30
+ }
31
+ /**
32
+ * Returns current active agent discovery directories.
33
+ */
34
+ getAgentDirs() {
35
+ return [...this.agentDirs];
36
+ }
37
+ /**
38
+ * Updates agent discovery directories and notifies subscribers.
39
+ */
40
+ setAgentDirs(dirs) {
41
+ this.agentDirs = [...dirs];
42
+ this.onDirsChanged?.(this.agentDirs);
43
+ }
44
+ /**
45
+ * Updates user-configured custom agent directories in store and triggers rescan.
46
+ */
47
+ async setCustomDirs(dirs) {
48
+ if (this.store) {
49
+ await this.store.setCustomAgentDirs(dirs);
50
+ this.broadcaster.broadcastState();
51
+ }
52
+ const resolved = resolveAgentDirectories({
53
+ customDirs: dirs,
54
+ rootDir: this.rootDir,
55
+ });
56
+ this.setAgentDirs(resolved);
57
+ await this.rescan();
58
+ }
59
+ /**
60
+ * Immediately re-reads agent definitions from disk and broadcasts catalogs.
61
+ */
62
+ async rescan() {
63
+ await Promise.all([
64
+ this.broadcastAgents(),
65
+ this.broadcastSubagents(),
66
+ ]);
24
67
  }
25
68
  /**
26
69
  * Fetches the model catalog from runtime, optionally tailored to a selected model.
@@ -39,23 +82,32 @@ export class CatalogService {
39
82
  * Reads one catalog filtered by mode at source.
40
83
  */
41
84
  async readAgentCatalog(mode) {
85
+ const primaryDir = this.agentDirs[0] ?? "";
42
86
  try {
43
- const scan = await readAgents(this.agentsDir, { mode });
87
+ const scan = await readAgents(this.agentDirs, { mode });
44
88
  for (const problem of scan.problems) {
45
- console.warn(`Super Pi: ${this.agentsDir}/${problem.file} is not a usable agent - ${problem.message}`);
89
+ console.warn(`Super Pi: ${problem.file} is not a usable agent - ${problem.message}`);
46
90
  }
47
91
  const agents = scan.agents.filter((agent) => !agent.hidden).map(summarize);
48
92
  if (!agents.length) {
49
- console.warn(`Super Pi: no ${mode} agents found in ${this.agentsDir} (${scan.problems.length} file(s) rejected).`);
93
+ console.warn(`Super Pi: no ${mode} agents found in [${this.agentDirs.join(", ")}] (${scan.problems.length} file(s) rejected).`);
50
94
  }
51
- return { source: "directory", agents, problems: scan.problems, directory: this.agentsDir, fetchedAt: new Date().toISOString() };
95
+ return {
96
+ source: "directory",
97
+ agents,
98
+ problems: scan.problems,
99
+ directory: primaryDir,
100
+ directories: [...this.agentDirs],
101
+ fetchedAt: new Date().toISOString(),
102
+ };
52
103
  }
53
104
  catch (error) {
54
105
  return {
55
106
  source: "unavailable",
56
107
  agents: [],
57
108
  problems: [],
58
- directory: this.agentsDir,
109
+ directory: primaryDir,
110
+ directories: [...this.agentDirs],
59
111
  fetchedAt: new Date().toISOString(),
60
112
  error: error instanceof Error ? error.message : "unknown error",
61
113
  };
@@ -27,8 +27,9 @@ export declare class ConversationService {
27
27
  private readonly catalogService;
28
28
  private readonly runService;
29
29
  private readonly extensionUiService;
30
- private readonly agentsDir;
31
- constructor(store: Store, runtime: AgentRuntime, subagents: SubagentRunRecorder, broadcaster: EventBroadcaster, catalogService: CatalogService, runService: RunService, extensionUiService: ExtensionUiService, agentsDir: string);
30
+ private agentsDir;
31
+ constructor(store: Store, runtime: AgentRuntime, subagents: SubagentRunRecorder, broadcaster: EventBroadcaster, catalogService: CatalogService, runService: RunService, extensionUiService: ExtensionUiService, agentsDir: string | string[]);
32
+ setAgentsDir(dir: string | string[]): void;
32
33
  /**
33
34
  * Creates a new conversation in a workspace.
34
35
  */
@@ -22,6 +22,9 @@ export class ConversationService {
22
22
  this.extensionUiService = extensionUiService;
23
23
  this.agentsDir = agentsDir;
24
24
  }
25
+ setAgentsDir(dir) {
26
+ this.agentsDir = dir;
27
+ }
25
28
  /**
26
29
  * Creates a new conversation in a workspace.
27
30
  */
@@ -101,7 +104,8 @@ export class ConversationService {
101
104
  }
102
105
  const agent = await this.runtime.agentFor?.({ id: conversation.id, agentId });
103
106
  if (!agent) {
104
- throw new Error(`Agent ${JSON.stringify(agentId)} 不存在或不是可用的 primary agent(目录:${this.agentsDir})`);
107
+ const dirsLabel = Array.isArray(this.agentsDir) ? this.agentsDir.join(", ") : this.agentsDir;
108
+ throw new Error(`Agent ${JSON.stringify(agentId)} 不存在或不是可用的 primary agent(目录:${dirsLabel})`);
105
109
  }
106
110
  if (agent.model) {
107
111
  const catalog = await this.runtime.catalog();
@@ -4,6 +4,7 @@ import type { SubagentRunRecorder } from "../subagent-runs.js";
4
4
  import type { Conversation, Message, MessageImage, PiQueueSnapshot, Workspace } from "../types.js";
5
5
  import type { EventBroadcaster } from "../server-context.js";
6
6
  import type { ExtensionUiService } from "./extension-ui-service.js";
7
+ import type { WorkflowEngine } from "../workflow-engine.js";
7
8
  export interface SubagentDispatchRequestBody {
8
9
  conversationId?: string;
9
10
  subagent_type?: string;
@@ -14,6 +15,23 @@ export interface SubagentDispatchRequestBody {
14
15
  description?: string;
15
16
  loopId?: string;
16
17
  role?: string;
18
+ /** The per-turn dispatch capability, presented by the primary agent's extension. See `capability.ts`. */
19
+ capability?: string;
20
+ }
21
+ /**
22
+ * A whole orchestrated plan, as it arrives over the wire.
23
+ *
24
+ * Deliberately NOT a `WorkflowPlan`: the plan type is what validation PRODUces, and accepting it as an
25
+ * input would let a caller assert that its steps were already checked. This is the untrusted shape, and
26
+ * `validateWorkflowPlan` is what turns one into a plan.
27
+ */
28
+ export interface WorkflowRunRequestBody {
29
+ conversationId?: string;
30
+ summary?: string;
31
+ onFailure?: string;
32
+ steps?: unknown;
33
+ /** The per-turn dispatch capability, presented by the primary agent's extension. See `capability.ts`. */
34
+ capability?: string;
17
35
  }
18
36
  /**
19
37
  * Service managing execution of conversation turns, message queueing,
@@ -25,8 +43,11 @@ export declare class RunService {
25
43
  private readonly subagents;
26
44
  private readonly broadcaster;
27
45
  private readonly extensionUiService;
46
+ private agentsDir;
47
+ private readonly workflows;
28
48
  private readonly activeRuns;
29
- constructor(store: Store, runtime: AgentRuntime, subagents: SubagentRunRecorder, broadcaster: EventBroadcaster, extensionUiService: ExtensionUiService);
49
+ constructor(store: Store, runtime: AgentRuntime, subagents: SubagentRunRecorder, broadcaster: EventBroadcaster, extensionUiService: ExtensionUiService, agentsDir: string | string[], workflows: WorkflowEngine);
50
+ setAgentsDir(dir: string | string[]): void;
30
51
  /**
31
52
  * Resolves the conversation and workspace synchronously for a run.
32
53
  */
@@ -104,10 +125,37 @@ export declare class RunService {
104
125
  status: number;
105
126
  body: unknown;
106
127
  }>;
128
+ /**
129
+ * Validates and executes one orchestrated plan to its end.
130
+ *
131
+ * BLOCKS, unlike `dispatchTask`'s background path, and that asymmetry is the whole design. A background
132
+ * dispatch returns immediately because the model is expected to keep working meanwhile; a plan is the
133
+ * model handing the work over and wanting the chain back, so the request stays open until the last step
134
+ * settles. The cost is a long-held connection - a three-step pipeline is minutes - which is the same
135
+ * cost `task` already carries, and is bounded by the same AbortSignal the extension already forwards,
136
+ * plus the wall-clock cap below.
137
+ *
138
+ * 200 FOR A PLAN THAT RAN, 422 FOR ONE THAT SETTLED AS SOMETHING ELSE - `completed` including deliberate
139
+ * skips, and anything else the terminal status can be. Same rule `dispatchTask` applies to a single run,
140
+ * applied to a whole plan: the transport reports whether the work concluded, and the body says how.
141
+ */
142
+ runWorkflow(body: WorkflowRunRequestBody): Promise<{
143
+ status: number;
144
+ body: unknown;
145
+ }>;
107
146
  /**
108
147
  * Stops a specific subagent run by runId.
109
148
  */
110
149
  stopSubagent(runId: string): void;
150
+ /**
151
+ * Stops one orchestrated plan, and says whether there was anything to stop.
152
+ *
153
+ * REACHES THE CHILD, not just the loop: the engine dispatches every step under one controller's signal,
154
+ * so aborting it reaches the child Pi and `killTree`s it exactly as the run recorder's stop does. A stop
155
+ * that only ended the loop would leave the sub-agent still running and still holding the workspace
156
+ * directory, which is the precise gap the run card's own stop button was added to close.
157
+ */
158
+ stopWorkflow(workflowId: string): boolean;
111
159
  /**
112
160
  * Stops all subagent runs belonging to a conversation.
113
161
  */
@@ -1,5 +1,8 @@
1
- import { checkSubagentDepth, parentDeniedTools } from "../task-tool.js";
1
+ import { checkSubagentDepth, parentDeniedTools, subagentRoster } from "../task-tool.js";
2
2
  import { newSubagentRunId } from "../subagents.js";
3
+ import { readAgents } from "../agents.js";
4
+ import { verifyCapability } from "../capability.js";
5
+ import { validateWorkflowPlan, workflowSummary } from "../workflow.js";
3
6
  /**
4
7
  * Service managing execution of conversation turns, message queueing,
5
8
  * follow-ups, and sub-agent dispatching.
@@ -10,13 +13,20 @@ export class RunService {
10
13
  subagents;
11
14
  broadcaster;
12
15
  extensionUiService;
16
+ agentsDir;
17
+ workflows;
13
18
  activeRuns = new Map();
14
- constructor(store, runtime, subagents, broadcaster, extensionUiService) {
19
+ constructor(store, runtime, subagents, broadcaster, extensionUiService, agentsDir, workflows) {
15
20
  this.store = store;
16
21
  this.runtime = runtime;
17
22
  this.subagents = subagents;
18
23
  this.broadcaster = broadcaster;
19
24
  this.extensionUiService = extensionUiService;
25
+ this.agentsDir = agentsDir;
26
+ this.workflows = workflows;
27
+ }
28
+ setAgentsDir(dir) {
29
+ this.agentsDir = dir;
20
30
  }
21
31
  /**
22
32
  * Resolves the conversation and workspace synchronously for a run.
@@ -63,6 +73,12 @@ export class RunService {
63
73
  if (this.subagents.stopConversation(conversationId)) {
64
74
  console.log(`Super Pi: stopped the sub-agent runs of conversation ${conversationId}.`);
65
75
  }
76
+ // Plans too, and separately: a plan's own loop holds the signal that reaches its current child, so
77
+ // stopping only the runs would let it dispatch the NEXT step a moment later. Reported on its own line
78
+ // because a person who pressed stop wants to know a plan was still marching, not just a card.
79
+ if (this.workflows.stopConversation(conversationId)) {
80
+ console.log(`Super Pi: stopped the running orchestration plans of conversation ${conversationId}.`);
81
+ }
66
82
  this.broadcaster.broadcastState();
67
83
  }
68
84
  /**
@@ -258,6 +274,12 @@ export class RunService {
258
274
  if (!conversationId || !subagentType || !task) {
259
275
  throw new Error("task requires conversationId, subagent_type and prompt");
260
276
  }
277
+ // BEFORE anything else, including before the conversation is looked up: a request that cannot prove it
278
+ // came from this server's primary agent gets no answer about what exists. Everything below this line
279
+ // is about shaping a dispatch; this line is about whether the caller may have one at all.
280
+ const capability = verifyCapability(conversationId, body.capability);
281
+ if (!capability.ok)
282
+ throw new Error(capability.message);
261
283
  if (body.depth !== undefined)
262
284
  checkSubagentDepth(body.depth);
263
285
  const { conversation, cwd } = this.dispatchContext(conversationId);
@@ -326,12 +348,69 @@ export class RunService {
326
348
  },
327
349
  };
328
350
  }
351
+ /**
352
+ * Validates and executes one orchestrated plan to its end.
353
+ *
354
+ * BLOCKS, unlike `dispatchTask`'s background path, and that asymmetry is the whole design. A background
355
+ * dispatch returns immediately because the model is expected to keep working meanwhile; a plan is the
356
+ * model handing the work over and wanting the chain back, so the request stays open until the last step
357
+ * settles. The cost is a long-held connection - a three-step pipeline is minutes - which is the same
358
+ * cost `task` already carries, and is bounded by the same AbortSignal the extension already forwards,
359
+ * plus the wall-clock cap below.
360
+ *
361
+ * 200 FOR A PLAN THAT RAN, 422 FOR ONE THAT SETTLED AS SOMETHING ELSE - `completed` including deliberate
362
+ * skips, and anything else the terminal status can be. Same rule `dispatchTask` applies to a single run,
363
+ * applied to a whole plan: the transport reports whether the work concluded, and the body says how.
364
+ */
365
+ async runWorkflow(body) {
366
+ const conversationId = body.conversationId?.trim();
367
+ if (!conversationId)
368
+ throw new Error("workflow requires conversationId");
369
+ // Same rule as `dispatchTask`, and for a larger reason: this endpoint triggers a SEQUENCE of
370
+ // dispatches, so a caller that cannot prove it is the primary agent would get an unattended pipeline
371
+ // of sub-agents writing to the workspace rather than one delegation.
372
+ const capability = verifyCapability(conversationId, body.capability);
373
+ if (!capability.ok)
374
+ throw new Error(capability.message);
375
+ const { conversation, cwd } = this.dispatchContext(conversationId);
376
+ // Validated against the roster THIS CALL reads from disk, for the same freshness reason the task tool
377
+ // builds its roster per turn: a sub-agent added while the server is running is dispatchable on the next
378
+ // message with no restart, because a person edits `agents/*.md` while the server runs.
379
+ const { agents } = await readAgents(this.agentsDir, { mode: "subagent" });
380
+ const validated = validateWorkflowPlan(body, subagentRoster(agents));
381
+ if (!validated.plan)
382
+ throw new Error(validated.refusal);
383
+ const { workflow } = await this.workflows.run({
384
+ conversationId: conversation.id,
385
+ cwd,
386
+ plan: validated.plan,
387
+ ...(conversation.model ? { model: conversation.model } : {}),
388
+ // The same inheritance a single dispatch applies: a parent's DENY reaches its children, its
389
+ // allowlist does not. Every step gets the primary's denials, exactly as one dispatch would.
390
+ denyTools: parentDeniedTools(await this.runtime.agentFor?.(conversation)),
391
+ });
392
+ return {
393
+ status: workflow.status === "completed" ? 200 : 422,
394
+ body: { workflow: workflowSummary(workflow) },
395
+ };
396
+ }
329
397
  /**
330
398
  * Stops a specific subagent run by runId.
331
399
  */
332
400
  stopSubagent(runId) {
333
401
  this.subagents.stop(runId);
334
402
  }
403
+ /**
404
+ * Stops one orchestrated plan, and says whether there was anything to stop.
405
+ *
406
+ * REACHES THE CHILD, not just the loop: the engine dispatches every step under one controller's signal,
407
+ * so aborting it reaches the child Pi and `killTree`s it exactly as the run recorder's stop does. A stop
408
+ * that only ended the loop would leave the sub-agent still running and still holding the workspace
409
+ * directory, which is the precise gap the run card's own stop button was added to close.
410
+ */
411
+ stopWorkflow(workflowId) {
412
+ return this.workflows.stop(workflowId);
413
+ }
335
414
  /**
336
415
  * Stops all subagent runs belonging to a conversation.
337
416
  */
@@ -23,8 +23,9 @@ export declare class WorkspaceService {
23
23
  private readonly runService;
24
24
  private readonly extensionUiService;
25
25
  private readonly uploadsDir;
26
- private readonly agentsDir;
27
- constructor(store: Store, runtime: AgentRuntime, broadcaster: EventBroadcaster, catalogService: CatalogService, runService: RunService, extensionUiService: ExtensionUiService, uploadsDir: string, agentsDir: string);
26
+ private agentsDir;
27
+ constructor(store: Store, runtime: AgentRuntime, broadcaster: EventBroadcaster, catalogService: CatalogService, runService: RunService, extensionUiService: ExtensionUiService, uploadsDir: string, agentsDir: string | string[]);
28
+ setAgentsDir(dir: string | string[]): void;
28
29
  /**
29
30
  * Creates a new workspace.
30
31
  */
@@ -21,6 +21,9 @@ export class WorkspaceService {
21
21
  this.uploadsDir = uploadsDir;
22
22
  this.agentsDir = agentsDir;
23
23
  }
24
+ setAgentsDir(dir) {
25
+ this.agentsDir = dir;
26
+ }
24
27
  /**
25
28
  * Creates a new workspace.
26
29
  */
@@ -91,7 +94,8 @@ export class WorkspaceService {
91
94
  }
92
95
  const agent = await this.runtime.agentFor({ id: workspaceId, agentId: settings.agentId });
93
96
  if (!agent) {
94
- throw new Error(`Agent ${JSON.stringify(settings.agentId)} 不存在或不是可用的 primary agent(目录:${this.agentsDir})`);
97
+ const dirsLabel = Array.isArray(this.agentsDir) ? this.agentsDir.join(", ") : this.agentsDir;
98
+ throw new Error(`Agent ${JSON.stringify(settings.agentId)} 不存在或不是可用的 primary agent(目录:${dirsLabel})`);
95
99
  }
96
100
  agentToStore = agent.id;
97
101
  }