@g3un/pi-orchestra 0.2.1 → 0.9.2

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.
@@ -1,8 +1,9 @@
1
+ import { isBusMessageDelivered, markBusMessagesDelivered, type BusMessage } from "../core/bus.ts";
1
2
  import type { AgentRun, AgentRunResult, AgentState } from "../core/subagent.ts";
2
3
  import type { AgentStore } from "../core/store.ts";
3
4
  import type { WorkflowRun } from "../core/workflow.ts";
4
5
  import type { WorkgroupStrategy } from "../core/workgroup.ts";
5
- import { formatNamedEntityLabel, isTerminalAgentState, toAgentRunResult } from "../utils.ts";
6
+ import { formatNamedEntityLabel, isAgentRunActive, isTerminalAgentState, toAgentRunResult } from "../utils.ts";
6
7
 
7
8
  export const ORCHESTRA_EVENT_CUSTOM_TYPE = "pi-orchestra.event";
8
9
 
@@ -22,6 +23,11 @@ export type OrchestraMainEvent =
22
23
  | {
23
24
  type: "workflow.finished";
24
25
  workflow: WorkflowRun;
26
+ }
27
+ | {
28
+ type: "bus.message";
29
+ busId: string;
30
+ message: BusMessage;
25
31
  };
26
32
 
27
33
  export interface WorkgroupRegistration {
@@ -33,8 +39,8 @@ export interface WorkgroupRegistration {
33
39
  export interface OrchestraEventControllerOptions {
34
40
  store: AgentStore;
35
41
  sendEvents: (events: OrchestraMainEvent[], content: string) => void;
36
- /** Defaults to 50 ms. Use 0 in tests for immediate delivery. */
37
- flushDelayMs?: number;
42
+ /** Defaults to 50 ms when undefined. Use 0 in tests for immediate delivery. */
43
+ flushDelayMs: number | undefined;
38
44
  }
39
45
 
40
46
  interface RegisteredWorkgroup {
@@ -47,16 +53,27 @@ interface LaunchingWorkgroup {
47
53
  finishedRunIds: Set<string>;
48
54
  }
49
55
 
56
+ interface BusMessageDelivery {
57
+ subscriptionId: string;
58
+ message: BusMessage;
59
+ }
60
+
61
+ interface FormatOrchestraEventsOptions {
62
+ formatBusMessageFrom: ((from: string) => string) | undefined;
63
+ }
64
+
50
65
  export class OrchestraEventController {
51
66
  private readonly store: AgentStore;
52
67
  private readonly sendEvents: OrchestraEventControllerOptions["sendEvents"];
53
68
  private readonly flushDelayMs: number;
54
- private readonly runStates = new Map<string, AgentState>();
69
+ private readonly runFinished = new Map<string, boolean>();
55
70
  private readonly workflowStates = new Map<string, AgentState>();
56
71
  private readonly mainWorkgroupsByBusId = new Map<string, RegisteredWorkgroup>();
57
72
  private readonly launchingWorkgroupsByBusId = new Map<string, LaunchingWorkgroup>();
58
73
  private readonly queuedEvents: OrchestraMainEvent[] = [];
74
+ private readonly queuedBusMessageDeliveries: BusMessageDelivery[] = [];
59
75
  private readonly unsubscribeRuns: () => void;
76
+ private readonly unsubscribeBusMessages: () => void;
60
77
  private readonly unsubscribeWorkflows: () => void;
61
78
  private flushTimer: ReturnType<typeof setTimeout> | undefined;
62
79
 
@@ -65,11 +82,18 @@ export class OrchestraEventController {
65
82
  this.sendEvents = options.sendEvents;
66
83
  this.flushDelayMs = options.flushDelayMs ?? 50;
67
84
 
68
- for (const run of this.store.listRuns()) this.runStates.set(run.id, run.state);
85
+ for (const run of this.store.listRuns()) this.runFinished.set(run.id, isAgentFinishRun(run));
69
86
  for (const workflow of this.store.listWorkflows()) this.workflowStates.set(workflow.id, workflow.state);
70
87
 
71
- this.unsubscribeRuns = this.store.subscribeRuns((run) => this.handleRunSaved(run));
72
- this.unsubscribeWorkflows = this.store.subscribeWorkflows((workflow) => this.handleWorkflowSaved(workflow));
88
+ this.unsubscribeRuns = this.store.subscribeRuns((run) => this.handleRunSaved(run), undefined);
89
+ this.unsubscribeBusMessages = this.store.subscribeBusMessages(
90
+ (event) => this.handleBusMessageSaved(event),
91
+ undefined,
92
+ );
93
+ this.unsubscribeWorkflows = this.store.subscribeWorkflows(
94
+ (workflow) => this.handleWorkflowSaved(workflow),
95
+ undefined,
96
+ );
73
97
  }
74
98
 
75
99
  beginWorkgroup(busId: string, strategy: WorkgroupStrategy): void {
@@ -98,7 +122,9 @@ export class OrchestraEventController {
98
122
  if (this.flushTimer) clearTimeout(this.flushTimer);
99
123
  this.flushTimer = undefined;
100
124
  this.queuedEvents.length = 0;
125
+ this.queuedBusMessageDeliveries.length = 0;
101
126
  this.unsubscribeRuns();
127
+ this.unsubscribeBusMessages();
102
128
  this.unsubscribeWorkflows();
103
129
  }
104
130
 
@@ -107,15 +133,23 @@ export class OrchestraEventController {
107
133
  this.flushTimer = undefined;
108
134
  if (this.queuedEvents.length === 0) return;
109
135
 
110
- const events = this.queuedEvents.splice(0);
111
- this.sendEvents(events, formatOrchestraEvents(events));
136
+ const events = [...this.queuedEvents];
137
+ const busMessageDeliveries = [...this.queuedBusMessageDeliveries];
138
+ this.sendEvents(
139
+ events,
140
+ formatOrchestraEvents(events, { formatBusMessageFrom: (from) => this.formatBusMessageFrom(from) }),
141
+ );
142
+ this.queuedEvents.splice(0, events.length);
143
+ this.queuedBusMessageDeliveries.splice(0, busMessageDeliveries.length);
144
+ for (const delivery of busMessageDeliveries) this.markBusMessageDelivered(delivery);
112
145
  }
113
146
 
114
147
  private handleRunSaved(run: AgentRun): void {
115
- const previousState = this.runStates.get(run.id);
116
- this.runStates.set(run.id, run.state);
148
+ const wasFinished = this.runFinished.get(run.id) ?? false;
149
+ const isFinished = isAgentFinishRun(run);
150
+ this.runFinished.set(run.id, isFinished);
117
151
 
118
- if (!isAgentFinishState(run.state) || isAgentFinishState(previousState)) return;
152
+ if (!isFinished || wasFinished) return;
119
153
  if (this.isWorkflowBus(run.busId)) return;
120
154
 
121
155
  const registeredWorkgroup = this.mainWorkgroupsByBusId.get(run.busId);
@@ -146,11 +180,29 @@ export class OrchestraEventController {
146
180
  this.queueEvent({ type: "subagent.finished", busId: run.busId, run: toAgentRunResult(run) });
147
181
  }
148
182
 
183
+ private handleBusMessageSaved(event: { busId: string; message: BusMessage }): void {
184
+ if (event.message.from === "main") return;
185
+
186
+ const subscriptions = this.store.listBusSubscriptions({
187
+ busId: event.busId,
188
+ subscriberId: "main",
189
+ subscriberKind: "main",
190
+ });
191
+ for (const subscription of subscriptions) {
192
+ if (isBusMessageDelivered(subscription, event.message.id)) continue;
193
+ if (this.hasQueuedBusMessageDelivery(subscription.id, event.message.id)) continue;
194
+
195
+ this.queuedBusMessageDeliveries.push({ subscriptionId: subscription.id, message: event.message });
196
+ this.queueEvent({ type: "bus.message", busId: event.busId, message: event.message });
197
+ }
198
+ }
199
+
149
200
  private handleWorkflowSaved(workflow: WorkflowRun): void {
150
201
  const previousState = this.workflowStates.get(workflow.id);
151
202
  this.workflowStates.set(workflow.id, workflow.state);
152
203
 
153
- if (!isTerminalAgentState(workflow.state) || isTerminalWorkflowState(previousState)) return;
204
+ if (!isTerminalAgentState(workflow.state) || (previousState !== undefined && isTerminalAgentState(previousState)))
205
+ return;
154
206
  this.queueEvent({ type: "workflow.finished", workflow });
155
207
  }
156
208
 
@@ -161,17 +213,35 @@ export class OrchestraEventController {
161
213
  private getPendingWorkgroupRunIds(busId: string, runIds: Set<string>): string[] {
162
214
  return this.store
163
215
  .listRuns()
164
- .filter((run) => run.busId === busId && runIds.has(run.id) && run.state === "idle")
216
+ .filter((run) => run.busId === busId && runIds.has(run.id) && isAgentRunActive(run))
165
217
  .map((run) => run.id);
166
218
  }
167
219
 
168
220
  private getPendingBusRunIds(busId: string): string[] {
169
221
  return this.store
170
222
  .listRuns()
171
- .filter((run) => run.busId === busId && run.state === "idle")
223
+ .filter((run) => run.busId === busId && isAgentRunActive(run))
172
224
  .map((run) => run.id);
173
225
  }
174
226
 
227
+ private hasQueuedBusMessageDelivery(subscriptionId: string, messageId: string): boolean {
228
+ return this.queuedBusMessageDeliveries.some(
229
+ (delivery) => delivery.subscriptionId === subscriptionId && delivery.message.id === messageId,
230
+ );
231
+ }
232
+
233
+ private formatBusMessageFrom(from: string): string {
234
+ if (from === "main") return from;
235
+ return this.store.getRun(from)?.name ?? from;
236
+ }
237
+
238
+ private markBusMessageDelivered(delivery: BusMessageDelivery): void {
239
+ const subscription = this.store.getBusSubscription(delivery.subscriptionId);
240
+ if (!subscription) return;
241
+
242
+ this.store.saveBusSubscription(markBusMessagesDelivered(subscription, delivery.message));
243
+ }
244
+
175
245
  private queueEvent(event: OrchestraMainEvent): void {
176
246
  this.queuedEvents.push(event);
177
247
  if (this.flushDelayMs === 0) {
@@ -183,28 +253,37 @@ export class OrchestraEventController {
183
253
  }
184
254
  }
185
255
 
186
- export function formatOrchestraEvents(events: OrchestraMainEvent[]): string {
256
+ export function formatOrchestraEvents(events: OrchestraMainEvent[], options?: FormatOrchestraEventsOptions): string {
187
257
  const headline = events.length === 1 ? "Pi-orchestra event:" : `Pi-orchestra events (${events.length}):`;
188
- return [headline, ...events.flatMap((event) => ["", formatOrchestraEvent(event)])].join("\n");
258
+ return [headline, ...events.flatMap((event) => ["", formatOrchestraEvent(event, options)])].join("\n");
189
259
  }
190
260
 
191
- function formatOrchestraEvent(event: OrchestraMainEvent): string {
261
+ function formatOrchestraEvent(event: OrchestraMainEvent, options: FormatOrchestraEventsOptions | undefined): string {
192
262
  if (event.type === "workflow.finished") return formatWorkflowFinishedEvent(event.workflow);
263
+ if (event.type === "bus.message") return formatBusMessageEvent(event, options);
193
264
 
194
265
  const runLabel = event.run.name === event.run.runId ? event.run.runId : `${event.run.name} (${event.run.runId})`;
195
266
  const lines =
196
267
  event.type === "workgroup.member_finished"
197
268
  ? [
198
- `- Workgroup member finished on bus ${event.busId}: ${runLabel} is ${event.run.state}.`,
269
+ `- Workgroup member finished on bus ${event.busId}: ${runLabel} ${formatRunFinishedState(event.run)}.`,
199
270
  ` Strategy: ${event.strategy}`,
200
271
  ` Pending workgroup run ids: ${event.pendingRunIds.length > 0 ? event.pendingRunIds.join(", ") : "none"}`,
201
272
  ]
202
- : [`- Subagent finished on bus ${event.busId}: ${runLabel} is ${event.run.state}.`];
273
+ : [`- Subagent finished on bus ${event.busId}: ${runLabel} ${formatRunFinishedState(event.run)}.`];
203
274
 
204
275
  lines.push(...formatRunResultLines(event.run));
205
276
  return lines.join("\n");
206
277
  }
207
278
 
279
+ function formatBusMessageEvent(
280
+ event: Extract<OrchestraMainEvent, { type: "bus.message" }>,
281
+ options: FormatOrchestraEventsOptions | undefined,
282
+ ): string {
283
+ const from = options?.formatBusMessageFrom?.(event.message.from) ?? event.message.from;
284
+ return [`- Bus message on ${event.busId} from ${from}:`, event.message.message].join("\n");
285
+ }
286
+
208
287
  function formatWorkflowFinishedEvent(workflow: WorkflowRun): string {
209
288
  const lines = [`- Workflow finished: ${formatNamedEntityLabel(workflow)} is ${workflow.state}.`];
210
289
  if (workflow.result) {
@@ -222,10 +301,14 @@ function formatRunResultLines(run: AgentRunResult): string[] {
222
301
  return lines;
223
302
  }
224
303
 
225
- function isAgentFinishState(state: AgentState | undefined): boolean {
226
- return state === "success" || state === "blocked" || state === "failed";
304
+ function formatRunFinishedState(run: AgentRunResult): string {
305
+ return run.result ? `finished with ${run.result.status}; state=${run.state}` : `is ${run.state}`;
227
306
  }
228
307
 
229
- function isTerminalWorkflowState(state: AgentState | undefined): boolean {
230
- return state !== undefined && isTerminalAgentState(state);
308
+ function isAgentFinishRun(run: AgentRun | AgentRunResult): boolean {
309
+ return run.result !== undefined || isAgentResultState(run.state);
310
+ }
311
+
312
+ function isAgentResultState(state: AgentState | undefined): boolean {
313
+ return state === "success" || state === "blocked" || state === "failed";
231
314
  }
@@ -2,7 +2,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AgentRun } from "../core/subagent.ts";
3
3
  import type { AgentStore } from "../core/store.ts";
4
4
  import type { WorkflowRun, WorkflowStageRun } from "../core/workflow.ts";
5
- import { isTerminalAgentState } from "../utils.ts";
5
+ import { isAgentRunFinished, isTerminalAgentState } from "../utils.ts";
6
6
 
7
7
  const WIDGET_KEY = "pi-orchestra.workflow-monitor";
8
8
  const MAX_MONITORED_WORKFLOWS = 2;
@@ -10,9 +10,9 @@ const MAX_WIDGET_LINES = 10;
10
10
  const DEFAULT_TICK_MS = 1_000;
11
11
 
12
12
  export interface WorkflowMonitorControllerOptions {
13
- now?: () => number;
14
- /** Defaults to 1000 ms. Use 0 to disable uptime ticks in tests. */
15
- tickMs?: number;
13
+ now: (() => number) | undefined;
14
+ /** Defaults to 1000 ms when undefined. Use 0 to disable uptime ticks in tests. */
15
+ tickMs: number | undefined;
16
16
  }
17
17
 
18
18
  export class WorkflowMonitorController {
@@ -24,7 +24,7 @@ export class WorkflowMonitorController {
24
24
 
25
25
  constructor(
26
26
  private readonly store: AgentStore,
27
- options: WorkflowMonitorControllerOptions = {},
27
+ options: WorkflowMonitorControllerOptions = { now: undefined, tickMs: undefined },
28
28
  ) {
29
29
  this.now = options.now ?? Date.now;
30
30
  this.tickMs = options.tickMs ?? DEFAULT_TICK_MS;
@@ -39,8 +39,8 @@ export class WorkflowMonitorController {
39
39
 
40
40
  this.ctx = ctx;
41
41
  if (!this.unsubscribe) {
42
- const unsubscribeRuns = this.store.subscribeRuns(() => this.render());
43
- const unsubscribeWorkflows = this.store.subscribeWorkflows(() => this.render());
42
+ const unsubscribeRuns = this.store.subscribeRuns(() => this.render(), undefined);
43
+ const unsubscribeWorkflows = this.store.subscribeWorkflows(() => this.render(), undefined);
44
44
  this.unsubscribe = () => {
45
45
  unsubscribeRuns();
46
46
  unsubscribeWorkflows();
@@ -112,10 +112,11 @@ function appendWorkflowLines(lines: string[], store: AgentStore, workflow: Workf
112
112
  if (lines.length >= MAX_WIDGET_LINES) return;
113
113
 
114
114
  const stage = getCurrentStage(workflow);
115
+ const workflowLabel = `${workflow.name} [${formatUptimeSince(workflow.startedAtMs, nowMs)}]`;
115
116
  const stageLabel = stage
116
- ? formatStageLabel(store, workflow, stage)
117
+ ? formatStageLabel(store, workflow, stage, nowMs)
117
118
  : `none (0/${workflow.stages.length}) | agents (0/0)`;
118
- lines.push(`${workflow.name} | ${stageLabel} | ${formatWorkflowUptime(workflow, nowMs)}`);
119
+ lines.push(`${workflowLabel} | ${stageLabel}`);
119
120
  }
120
121
 
121
122
  function listActiveWorkflows(store: AgentStore): WorkflowRun[] {
@@ -128,10 +129,11 @@ function getCurrentStage(workflow: WorkflowRun): WorkflowStageRun | undefined {
128
129
  );
129
130
  }
130
131
 
131
- function formatStageLabel(store: AgentStore, workflow: WorkflowRun, stage: WorkflowStageRun): string {
132
+ function formatStageLabel(store: AgentStore, workflow: WorkflowRun, stage: WorkflowStageRun, nowMs: number): string {
132
133
  const stageIndex = workflow.stages.indexOf(stage);
133
134
  const stagePosition = stageIndex >= 0 ? `${stageIndex + 1}/${workflow.stages.length}` : `?/${workflow.stages.length}`;
134
- return `${stage.name} (${stagePosition}) | agents (${formatStageProgress(store, stage)})`;
135
+ const stageUptime = formatUptimeSince(stage.startedAtMs, nowMs);
136
+ return `${stage.name} [${stageUptime}] (${stagePosition}) | agents (${formatStageProgress(store, stage)})`;
135
137
  }
136
138
 
137
139
  function formatStageProgress(store: AgentStore, stage: WorkflowStageRun): string {
@@ -139,8 +141,8 @@ function formatStageProgress(store: AgentStore, stage: WorkflowStageRun): string
139
141
  return `${progress.completed}/${progress.total}`;
140
142
  }
141
143
 
142
- function formatWorkflowUptime(workflow: WorkflowRun, nowMs: number): string {
143
- const elapsedSeconds = Math.max(0, Math.floor((nowMs - workflow.startedAtMs) / 1_000));
144
+ function formatUptimeSince(startedAtMs: number, nowMs: number): string {
145
+ const elapsedSeconds = Math.max(0, Math.floor((nowMs - startedAtMs) / 1_000));
144
146
  const seconds = elapsedSeconds % 60;
145
147
  const totalMinutes = Math.floor(elapsedSeconds / 60);
146
148
  const minutes = totalMinutes % 60;
@@ -157,7 +159,7 @@ function pad2(value: number): string {
157
159
 
158
160
  function calculateStageProgress(store: AgentStore, stage: WorkflowStageRun): { completed: number; total: number } {
159
161
  const runs = collectStageRuns(store, stage);
160
- const completed = runs.filter((run) => isTerminalAgentState(run.state)).length;
162
+ const completed = runs.filter(isAgentRunFinished).length;
161
163
  const workerRunCount = runs.filter((run) => run.id !== stage.leaderRunId).length;
162
164
  const workerTotal = Math.max(stage.members.length, stage.workerRunIds.length, workerRunCount);
163
165
  const leaderTotal = stage.phase === "leader" || stage.leaderRunId !== undefined ? 1 : 0;
@@ -0,0 +1,20 @@
1
+ import type { AgentProfile } from "../core/subagent.ts";
2
+ import { defineAgentProfile, type ToolProfileOptions } from "./profile.ts";
3
+
4
+ export interface CodeReviewerProfileOptions extends ToolProfileOptions {}
5
+
6
+ export function createCodeReviewerProfile(options: CodeReviewerProfileOptions): AgentProfile {
7
+ return defineAgentProfile({
8
+ defaultName: "code-reviewer",
9
+ systemPrompt: [
10
+ "You are a code reviewer: identify bugs, behavioral regressions, security risks, and missing tests in the assigned local code or change.",
11
+ "Prioritize findings over summary. For each finding, include severity, affected file/symbol or line reference, impact, and a concrete remediation direction.",
12
+ "Use local repository evidence and supplied task context; treat bus context as supplemental and note conflicts.",
13
+ "Do not modify files or perform unrelated implementation work, even if editing tools are present.",
14
+ "If no material issues are found, say so clearly and note residual test gaps or review limits.",
15
+ "Use status=success when review findings or a no-issues conclusion are supported, blocked when the target/diff/context/tools are insufficient, and failed when review execution fails.",
16
+ ],
17
+ tools: options.tools,
18
+ options,
19
+ });
20
+ }
@@ -0,0 +1,20 @@
1
+ import type { AgentProfile } from "../core/subagent.ts";
2
+ import { defineAgentProfile, type ToolProfileOptions } from "./profile.ts";
3
+
4
+ export interface EvidenceSynthesizerProfileOptions extends ToolProfileOptions {}
5
+
6
+ export function createEvidenceSynthesizerProfile(options: EvidenceSynthesizerProfileOptions): AgentProfile {
7
+ return defineAgentProfile({
8
+ defaultName: "evidence-synthesizer",
9
+ systemPrompt: [
10
+ "You are an evidence synthesizer: produce concise canonical output from supplied worker results and shared context.",
11
+ "Treat supplied context (workflow/stage goals, previous outputs, worker results, bus context) as primary evidence.",
12
+ "Use allowed tools only to verify evidence or resolve concrete gaps needed for synthesis; do not broaden scope or perform unrelated work.",
13
+ "Do not modify files or external state; if necessary tools/context are unavailable, report blocked instead of guessing.",
14
+ "Deduplicate and reconcile evidence; note conflicts/gaps; prefer finish results over bus context.",
15
+ "Prefer status=success if useful output exists; use blocked if context is insufficient, failed if synthesis fails.",
16
+ ],
17
+ tools: options.tools,
18
+ options,
19
+ });
20
+ }
@@ -0,0 +1,20 @@
1
+ import type { AgentProfile } from "../core/subagent.ts";
2
+ import { defineAgentProfile, type ToolProfileOptions } from "./profile.ts";
3
+
4
+ export interface ExternalResearcherProfileOptions extends ToolProfileOptions {}
5
+
6
+ export function createExternalResearcherProfile(options: ExternalResearcherProfileOptions): AgentProfile {
7
+ return defineAgentProfile({
8
+ defaultName: "external-researcher",
9
+ systemPrompt: [
10
+ "You are an external research agent: gather, verify, and synthesize information from outside the target repository.",
11
+ "Use the supplied task and bus context to define scope; prefer official and primary sources, then clearly label secondary sources.",
12
+ "Record source names, links, publication or version dates when available, and distinguish current facts from inference.",
13
+ "Compare similar projects only when the assignment asks; otherwise stay focused on the named target.",
14
+ "Do not modify local files. If required search/fetch/browse tools or credible sources are unavailable, finish with status=blocked instead of guessing.",
15
+ "Finish with concise findings, cited source list, conflicts/gaps, confidence, and useful structured data when comparison or source tables help.",
16
+ ],
17
+ tools: options.tools,
18
+ options,
19
+ });
20
+ }
@@ -1 +1,5 @@
1
- export * from "./stage-leader.ts";
1
+ export * from "./code-reviewer.ts";
2
+ export * from "./evidence-synthesizer.ts";
3
+ export * from "./external-researcher.ts";
4
+ export * from "./profile.ts";
5
+ export * from "./source-code-qa.ts";
@@ -0,0 +1,31 @@
1
+ import type { AgentProfile } from "../core/subagent.ts";
2
+
3
+ export interface BaseProfileOptions {
4
+ name: string | undefined;
5
+ model: string | undefined;
6
+ }
7
+
8
+ export interface ToolProfileOptions extends BaseProfileOptions {
9
+ tools: string[];
10
+ }
11
+
12
+ interface DefineAgentProfileOptions {
13
+ defaultName: string;
14
+ systemPrompt: readonly string[];
15
+ tools: readonly string[];
16
+ options: BaseProfileOptions;
17
+ }
18
+
19
+ export function defineAgentProfile({
20
+ defaultName,
21
+ systemPrompt,
22
+ tools,
23
+ options,
24
+ }: DefineAgentProfileOptions): AgentProfile {
25
+ return {
26
+ name: options.name ?? defaultName,
27
+ systemPrompt: systemPrompt.join("\n"),
28
+ tools: [...tools],
29
+ model: options.model,
30
+ };
31
+ }
@@ -0,0 +1,19 @@
1
+ import type { AgentProfile } from "../core/subagent.ts";
2
+ import { defineAgentProfile, type ToolProfileOptions } from "./profile.ts";
3
+
4
+ export interface SourceCodeQaProfileOptions extends ToolProfileOptions {}
5
+
6
+ export function createSourceCodeQaProfile(options: SourceCodeQaProfileOptions): AgentProfile {
7
+ return defineAgentProfile({
8
+ defaultName: "source-code-qa",
9
+ systemPrompt: [
10
+ "You are a source-code QA agent: answer questions using the target repository's source code, tests, and local docs.",
11
+ "Treat repository files and explicit task context as authoritative; treat bus context as supplemental unless the task says otherwise.",
12
+ "Inspect only the local project and supplied context; do not perform external research or modify files.",
13
+ "Finish with a concise answer, evidence paths/symbols or line references when useful, assumptions, and unresolved gaps.",
14
+ "Use status=success when the answer is supported, blocked when required code/context/tools are unavailable, and failed when inspection fails.",
15
+ ],
16
+ tools: options.tools,
17
+ options,
18
+ });
19
+ }