@forwardimpact/libharness 1.3.0 → 1.4.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/bin/fit-harness.js +18 -0
- package/package.json +1 -1
- package/src/advisor.js +218 -0
- package/src/agent-runner.js +6 -0
- package/src/benchmark/report.js +9 -1
- package/src/commands/advisor-flags.js +28 -0
- package/src/commands/benchmark-definition.js +5 -0
- package/src/commands/benchmark-report.js +12 -2
- package/src/commands/discuss.js +4 -0
- package/src/commands/facilitate.js +4 -0
- package/src/commands/run.js +162 -67
- package/src/commands/supervise.js +4 -0
- package/src/discuss-tools.js +2 -1
- package/src/discusser.js +65 -10
- package/src/facilitator.js +64 -9
- package/src/index.js +9 -0
- package/src/orchestration-toolkit.js +65 -7
- package/src/supervisor.js +72 -10
- package/src/transcript-recorder.js +94 -0
package/src/commands/run.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { Writable } from "node:stream";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { isoTimestamp } from "@forwardimpact/libutil";
|
|
4
|
+
import { createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
4
5
|
import { createAgentRunner } from "../agent-runner.js";
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
advisorGuidance,
|
|
8
|
+
createAdvisor,
|
|
9
|
+
createAdvisorBudget,
|
|
10
|
+
} from "../advisor.js";
|
|
11
|
+
import { advisorTool } from "../orchestration-toolkit.js";
|
|
12
|
+
import {
|
|
13
|
+
composeProfilePrompt,
|
|
14
|
+
composeSystemPrompt,
|
|
15
|
+
} from "../profile-prompt.js";
|
|
6
16
|
import { createRedactor } from "../redaction.js";
|
|
7
17
|
import { createTeeWriter } from "../tee-writer.js";
|
|
18
|
+
import { createTranscriptRecorder } from "../transcript-recorder.js";
|
|
8
19
|
import { SequenceCounter } from "../sequence-counter.js";
|
|
20
|
+
import { parseAdvisorOptions } from "./advisor-flags.js";
|
|
9
21
|
import { resolveWorkTracker } from "./work-tracker.js";
|
|
10
22
|
import { resolveTaskContent } from "./task-input.js";
|
|
11
23
|
import { createServiceConfig } from "@forwardimpact/libconfig";
|
|
@@ -15,7 +27,7 @@ import { AGENT_MODEL } from "@forwardimpact/libutil/models";
|
|
|
15
27
|
* Parse and validate run command options from parsed values.
|
|
16
28
|
* @param {object} values - Parsed option values from cli.parse()
|
|
17
29
|
* @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
|
|
18
|
-
* @returns {{ taskContent: string, cwd: string,
|
|
30
|
+
* @returns {{ taskContent: string, taskAmend: string|undefined, cwd: string, agentModel: string, maxTurns: number, outputPath: string|undefined, agentProfile: string|undefined, workTracker: string, allowedTools: string[], mcpServer: string|undefined, advisorModel: string|undefined, advisorMaxUses: number }}
|
|
19
31
|
*/
|
|
20
32
|
export function parseRunOptions(values, runtime) {
|
|
21
33
|
const { task: taskContent, amend: taskAmend } = resolveTaskContent(
|
|
@@ -40,9 +52,148 @@ export function parseRunOptions(values, runtime) {
|
|
|
40
52
|
"Bash,Read,Glob,Grep,Write,Edit,Agent,TodoWrite"
|
|
41
53
|
).split(","),
|
|
42
54
|
mcpServer: values["mcp-server"] || undefined,
|
|
55
|
+
...parseAdvisorOptions(values),
|
|
43
56
|
};
|
|
44
57
|
}
|
|
45
58
|
|
|
59
|
+
const devNull = new Writable({
|
|
60
|
+
write(_chunk, _enc, cb) {
|
|
61
|
+
cb();
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Wire the run-mode agent session: external MCP entry, `LIBHARNESS_*` env
|
|
67
|
+
* writes, system-prompt composition, and — when an advisor model is set —
|
|
68
|
+
* the advisor wiring (budget, recorder, advisor session, dedicated MCP
|
|
69
|
+
* server holding only the `Advisor` tool). Extracted from `runRunCommand`
|
|
70
|
+
* so tests can inject a fake `query`.
|
|
71
|
+
*
|
|
72
|
+
* Run mode has no stop path (the command simply awaits the runner), so the
|
|
73
|
+
* consult timeout is deliberately the advisor's only guard.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} deps
|
|
76
|
+
* @param {ReturnType<typeof parseRunOptions>} deps.opts
|
|
77
|
+
* @param {import("../redaction.js").Redactor} deps.redactor
|
|
78
|
+
* @param {import("stream").Writable} deps.output - Envelope NDJSON sink.
|
|
79
|
+
* @param {SequenceCounter} deps.counter
|
|
80
|
+
* @param {function} deps.query - SDK query function.
|
|
81
|
+
* @param {import("@forwardimpact/libutil/runtime").Runtime} deps.runtime
|
|
82
|
+
* @returns {Promise<{runner: import("../agent-runner.js").AgentRunner, advisor: object|null}>}
|
|
83
|
+
*/
|
|
84
|
+
export async function wireRunSession({
|
|
85
|
+
opts,
|
|
86
|
+
redactor,
|
|
87
|
+
output,
|
|
88
|
+
counter,
|
|
89
|
+
query,
|
|
90
|
+
runtime,
|
|
91
|
+
}) {
|
|
92
|
+
const emitEnvelope = (source, event) => {
|
|
93
|
+
output.write(
|
|
94
|
+
JSON.stringify(
|
|
95
|
+
redactor.redactValue({ source, seq: counter.next(), event }),
|
|
96
|
+
) + "\n",
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
const onLine = (line) => emitEnvelope("agent", JSON.parse(line));
|
|
100
|
+
|
|
101
|
+
let mcpServers = null;
|
|
102
|
+
const allowedTools = opts.allowedTools;
|
|
103
|
+
if (opts.mcpServer) {
|
|
104
|
+
const mcpConfig = await createServiceConfig("mcp");
|
|
105
|
+
mcpServers = {
|
|
106
|
+
[opts.mcpServer]: {
|
|
107
|
+
type: "http",
|
|
108
|
+
url: mcpConfig.url,
|
|
109
|
+
headers: { Authorization: `Bearer ${mcpConfig.mcpToken()}` },
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
allowedTools.push(`mcp__${opts.mcpServer}__*`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (opts.agentProfile) {
|
|
116
|
+
runtime.proc.env.LIBHARNESS_AGENT_PROFILE = opts.agentProfile;
|
|
117
|
+
}
|
|
118
|
+
// Unconditional so the default "github" is observable to the agent's
|
|
119
|
+
// active-tracker resolution, mirroring --agent-profile's env write above.
|
|
120
|
+
runtime.proc.env.LIBHARNESS_WORK_TRACKER = opts.workTracker;
|
|
121
|
+
|
|
122
|
+
// With a profile, the consult guidance rides the profile composer's
|
|
123
|
+
// amendment parameter; with no profile, a preset-append prompt carries
|
|
124
|
+
// the guidance as its only session-protocol fragment. Advisor off and no
|
|
125
|
+
// profile means no system prompt — today's behavior, unchanged.
|
|
126
|
+
let systemPrompt;
|
|
127
|
+
if (opts.agentProfile) {
|
|
128
|
+
systemPrompt = composeProfilePrompt(opts.agentProfile, {
|
|
129
|
+
profilesDir: resolve(opts.cwd, ".claude/agents"),
|
|
130
|
+
runtime,
|
|
131
|
+
...(opts.advisorModel && {
|
|
132
|
+
amend: advisorGuidance(opts.advisorMaxUses),
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
} else if (opts.advisorModel) {
|
|
136
|
+
systemPrompt = composeSystemPrompt({
|
|
137
|
+
role: "agent",
|
|
138
|
+
trailer: advisorGuidance(opts.advisorMaxUses),
|
|
139
|
+
runtime,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let advisor = null;
|
|
144
|
+
let recorder = null;
|
|
145
|
+
if (opts.advisorModel) {
|
|
146
|
+
const budget = createAdvisorBudget(opts.advisorMaxUses);
|
|
147
|
+
recorder = createTranscriptRecorder({ systemPrompt, redactor });
|
|
148
|
+
advisor = createAdvisor({
|
|
149
|
+
model: opts.advisorModel,
|
|
150
|
+
cwd: opts.cwd,
|
|
151
|
+
query,
|
|
152
|
+
recorder,
|
|
153
|
+
redactor,
|
|
154
|
+
runtime,
|
|
155
|
+
onLine: (line) => emitEnvelope("advisor", JSON.parse(line)),
|
|
156
|
+
});
|
|
157
|
+
const advTool = advisorTool({
|
|
158
|
+
from: "agent",
|
|
159
|
+
consult: (q) => advisor.consult(q),
|
|
160
|
+
emit: (event) => emitEnvelope("orchestrator", event),
|
|
161
|
+
budget,
|
|
162
|
+
model: opts.advisorModel,
|
|
163
|
+
});
|
|
164
|
+
// No allowlist push: in-process SDK MCP servers work under
|
|
165
|
+
// bypassPermissions without allowlist entries (loop-mode precedent).
|
|
166
|
+
mcpServers = {
|
|
167
|
+
...mcpServers,
|
|
168
|
+
advisor: createSdkMcpServer({ name: "advisor", tools: [advTool] }),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const runner = createAgentRunner({
|
|
173
|
+
cwd: opts.cwd,
|
|
174
|
+
query,
|
|
175
|
+
output: devNull,
|
|
176
|
+
model: opts.agentModel,
|
|
177
|
+
maxTurns: opts.maxTurns,
|
|
178
|
+
allowedTools,
|
|
179
|
+
onLine: recorder
|
|
180
|
+
? (line) => {
|
|
181
|
+
onLine(line);
|
|
182
|
+
recorder.recordMessage(line);
|
|
183
|
+
}
|
|
184
|
+
: onLine,
|
|
185
|
+
...(recorder && { onPrompt: (text) => recorder.recordPrompt(text) }),
|
|
186
|
+
settingSources: ["project"],
|
|
187
|
+
systemPrompt,
|
|
188
|
+
taskAmend: opts.taskAmend,
|
|
189
|
+
mcpServers,
|
|
190
|
+
redactor,
|
|
191
|
+
runtime,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return { runner, advisor };
|
|
195
|
+
}
|
|
196
|
+
|
|
46
197
|
/**
|
|
47
198
|
* Run command — execute a single agent via the Claude Agent SDK.
|
|
48
199
|
*
|
|
@@ -53,18 +204,7 @@ export function parseRunOptions(values, runtime) {
|
|
|
53
204
|
*/
|
|
54
205
|
export async function runRunCommand(ctx) {
|
|
55
206
|
const runtime = ctx.deps.runtime;
|
|
56
|
-
const
|
|
57
|
-
taskContent,
|
|
58
|
-
taskAmend,
|
|
59
|
-
cwd,
|
|
60
|
-
agentModel,
|
|
61
|
-
maxTurns,
|
|
62
|
-
outputPath,
|
|
63
|
-
agentProfile,
|
|
64
|
-
workTracker,
|
|
65
|
-
allowedTools,
|
|
66
|
-
mcpServer,
|
|
67
|
-
} = parseRunOptions(ctx.options, runtime);
|
|
207
|
+
const opts = parseRunOptions(ctx.options, runtime);
|
|
68
208
|
|
|
69
209
|
// Build the redactor as the first observable side-effect after option
|
|
70
210
|
// parsing — the env snapshot must freeze BEFORE any in-process
|
|
@@ -73,8 +213,8 @@ export async function runRunCommand(ctx) {
|
|
|
73
213
|
|
|
74
214
|
// When --output is specified, stream text to stdout while writing NDJSON to file.
|
|
75
215
|
// Otherwise, write NDJSON directly to stdout (backwards-compatible).
|
|
76
|
-
const fileStream = outputPath
|
|
77
|
-
? runtime.fs.createWriteStream(outputPath)
|
|
216
|
+
const fileStream = opts.outputPath
|
|
217
|
+
? runtime.fs.createWriteStream(opts.outputPath)
|
|
78
218
|
: null;
|
|
79
219
|
const output = fileStream
|
|
80
220
|
? createTeeWriter({
|
|
@@ -86,62 +226,17 @@ export async function runRunCommand(ctx) {
|
|
|
86
226
|
: runtime.proc.stdout;
|
|
87
227
|
|
|
88
228
|
const counter = new SequenceCounter();
|
|
89
|
-
const devNull = new Writable({
|
|
90
|
-
write(_chunk, _enc, cb) {
|
|
91
|
-
cb();
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
const onLine = (line) => {
|
|
95
|
-
const event = JSON.parse(line);
|
|
96
|
-
const tagged = { source: "agent", seq: counter.next(), event };
|
|
97
|
-
output.write(JSON.stringify(redactor.redactValue(tagged)) + "\n");
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
let mcpServers = null;
|
|
101
|
-
if (mcpServer) {
|
|
102
|
-
const mcpConfig = await createServiceConfig("mcp");
|
|
103
|
-
mcpServers = {
|
|
104
|
-
[mcpServer]: {
|
|
105
|
-
type: "http",
|
|
106
|
-
url: mcpConfig.url,
|
|
107
|
-
headers: { Authorization: `Bearer ${mcpConfig.mcpToken()}` },
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
allowedTools.push(`mcp__${mcpServer}__*`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (agentProfile) {
|
|
114
|
-
runtime.proc.env.LIBHARNESS_AGENT_PROFILE = agentProfile;
|
|
115
|
-
}
|
|
116
|
-
// Unconditional so the default "github" is observable to the agent's
|
|
117
|
-
// active-tracker resolution, mirroring --agent-profile's env write above.
|
|
118
|
-
runtime.proc.env.LIBHARNESS_WORK_TRACKER = workTracker;
|
|
119
|
-
|
|
120
|
-
const systemPrompt = agentProfile
|
|
121
|
-
? composeProfilePrompt(agentProfile, {
|
|
122
|
-
profilesDir: resolve(cwd, ".claude/agents"),
|
|
123
|
-
runtime,
|
|
124
|
-
})
|
|
125
|
-
: undefined;
|
|
126
|
-
|
|
127
229
|
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
128
|
-
const runner =
|
|
129
|
-
|
|
130
|
-
query,
|
|
131
|
-
output: devNull,
|
|
132
|
-
model: agentModel,
|
|
133
|
-
maxTurns,
|
|
134
|
-
allowedTools,
|
|
135
|
-
onLine,
|
|
136
|
-
settingSources: ["project"],
|
|
137
|
-
systemPrompt,
|
|
138
|
-
taskAmend,
|
|
139
|
-
mcpServers,
|
|
230
|
+
const { runner } = await wireRunSession({
|
|
231
|
+
opts,
|
|
140
232
|
redactor,
|
|
233
|
+
output,
|
|
234
|
+
counter,
|
|
235
|
+
query,
|
|
141
236
|
runtime,
|
|
142
237
|
});
|
|
143
238
|
|
|
144
|
-
const result = await runner.run(taskContent);
|
|
239
|
+
const result = await runner.run(opts.taskContent);
|
|
145
240
|
|
|
146
241
|
if (fileStream) {
|
|
147
242
|
await new Promise((r) => output.end(r));
|
|
@@ -3,6 +3,7 @@ import { isoTimestamp } from "@forwardimpact/libutil";
|
|
|
3
3
|
import { createSupervisor } from "../supervisor.js";
|
|
4
4
|
import { createRedactor } from "../redaction.js";
|
|
5
5
|
import { createTeeWriter } from "../tee-writer.js";
|
|
6
|
+
import { parseAdvisorOptions } from "./advisor-flags.js";
|
|
6
7
|
import { resolveTaskContent } from "./task-input.js";
|
|
7
8
|
import { resolveWorkTracker } from "./work-tracker.js";
|
|
8
9
|
import { createServiceConfig } from "@forwardimpact/libconfig";
|
|
@@ -52,6 +53,7 @@ export async function parseSuperviseOptions(values, runtime) {
|
|
|
52
53
|
? supervisorAllowedToolsRaw.split(",")
|
|
53
54
|
: undefined,
|
|
54
55
|
mcpServer: values["mcp-server"] || undefined,
|
|
56
|
+
...parseAdvisorOptions(values),
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -125,6 +127,8 @@ export async function runSuperviseCommand(ctx) {
|
|
|
125
127
|
agentMcpServers,
|
|
126
128
|
redactor,
|
|
127
129
|
runtime,
|
|
130
|
+
advisorModel: opts.advisorModel,
|
|
131
|
+
advisorMaxUses: opts.advisorMaxUses,
|
|
128
132
|
});
|
|
129
133
|
|
|
130
134
|
const result = await supervisor.run(opts.taskContent);
|
package/src/discuss-tools.js
CHANGED
|
@@ -107,7 +107,7 @@ const ACKNOWLEDGE_DESC =
|
|
|
107
107
|
"Acknowledge an Ask before starting work. Posts a visible comment on the thread. Does not discharge the Ask — you still owe an Answer.";
|
|
108
108
|
|
|
109
109
|
/** Discuss-mode agent tool server. */
|
|
110
|
-
export function createDiscussAgentToolServer(ctx, { from }) {
|
|
110
|
+
export function createDiscussAgentToolServer(ctx, { from, extraTools = [] }) {
|
|
111
111
|
return orchestrationServer([
|
|
112
112
|
...baseTools(ctx, { from, defaultTo: "lead", broadcast: true }),
|
|
113
113
|
requestForCommentTool(ctx),
|
|
@@ -133,6 +133,7 @@ export function createDiscussAgentToolServer(ctx, { from }) {
|
|
|
133
133
|
return { content: [{ type: "text", text: "Acknowledged." }] };
|
|
134
134
|
},
|
|
135
135
|
),
|
|
136
|
+
...extraTools,
|
|
136
137
|
]);
|
|
137
138
|
}
|
|
138
139
|
|
package/src/discusser.js
CHANGED
|
@@ -22,7 +22,16 @@ import { ReplyEmitter } from "./reply-emitter.js";
|
|
|
22
22
|
import { composeSystemPrompt } from "./profile-prompt.js";
|
|
23
23
|
import { SequenceCounter } from "./sequence-counter.js";
|
|
24
24
|
import { createMessageBus } from "./message-bus.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
advisorTool,
|
|
27
|
+
createOrchestrationContext,
|
|
28
|
+
} from "./orchestration-toolkit.js";
|
|
29
|
+
import {
|
|
30
|
+
createAdvisor,
|
|
31
|
+
createAdvisorBudget,
|
|
32
|
+
withAdvisorGuidance,
|
|
33
|
+
} from "./advisor.js";
|
|
34
|
+
import { createTranscriptRecorder } from "./transcript-recorder.js";
|
|
26
35
|
import {
|
|
27
36
|
createDiscussLeadToolServer,
|
|
28
37
|
createDiscussAgentToolServer,
|
|
@@ -206,6 +215,8 @@ export class Discusser {
|
|
|
206
215
|
* @param {string|null} [deps.callbackUrl]
|
|
207
216
|
* @param {string|null} [deps.inboxUrl]
|
|
208
217
|
* @param {string|null} [deps.correlationId]
|
|
218
|
+
* @param {string} [deps.advisorModel] - Claude model for advisor consults; absent means no Advisor tool is offered.
|
|
219
|
+
* @param {number} [deps.advisorMaxUses] - Session-wide consult budget shared by all agent participants (default 3).
|
|
209
220
|
* @returns {Discusser}
|
|
210
221
|
*/
|
|
211
222
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: factory wires N runners + resume hydration paths
|
|
@@ -228,6 +239,8 @@ export function createDiscusser({
|
|
|
228
239
|
inboxUrl,
|
|
229
240
|
correlationId,
|
|
230
241
|
runtime,
|
|
242
|
+
advisorModel,
|
|
243
|
+
advisorMaxUses,
|
|
231
244
|
}) {
|
|
232
245
|
if (!redactor) throw new Error("redactor is required");
|
|
233
246
|
if (!runtime) throw new Error("runtime is required");
|
|
@@ -306,11 +319,54 @@ export function createDiscusser({
|
|
|
306
319
|
let discusser;
|
|
307
320
|
const leadServer = createDiscussLeadToolServer(ctx);
|
|
308
321
|
|
|
322
|
+
// One budget per session, shared by every agent's Advisor handler.
|
|
323
|
+
const budget = advisorModel ? createAdvisorBudget(advisorMaxUses ?? 3) : null;
|
|
324
|
+
|
|
309
325
|
const agents = resolvedConfigs.map((config) => {
|
|
326
|
+
// Everything advisor-shaped is gated on advisorModel; with it unset the
|
|
327
|
+
// composed prompt and tool surface are byte-identical to today's.
|
|
328
|
+
const systemPrompt = composeSystemPrompt({
|
|
329
|
+
role: "agent",
|
|
330
|
+
profile: config.agentProfile,
|
|
331
|
+
profilesDir: resolvedProfilesDir,
|
|
332
|
+
trailer: DISCUSS_AGENT_SYSTEM_PROMPT,
|
|
333
|
+
amend: withAdvisorGuidance(config.systemPromptAmend, budget),
|
|
334
|
+
runtime,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
let recorder = null;
|
|
338
|
+
let extraTools;
|
|
339
|
+
if (advisorModel) {
|
|
340
|
+
recorder = createTranscriptRecorder({ systemPrompt, redactor });
|
|
341
|
+
// Late-bound through the `let discusser` closure — the instance does
|
|
342
|
+
// not exist yet when the advisor and tool are built.
|
|
343
|
+
const advisor = createAdvisor({
|
|
344
|
+
model: advisorModel,
|
|
345
|
+
cwd: config.cwd ?? resolvedLeadCwd,
|
|
346
|
+
query,
|
|
347
|
+
recorder,
|
|
348
|
+
redactor,
|
|
349
|
+
runtime,
|
|
350
|
+
onLine: (line) => discusser.loop.emitLine("advisor", line),
|
|
351
|
+
});
|
|
352
|
+
abortController.signal.addEventListener("abort", () => advisor.abort());
|
|
353
|
+
extraTools = [
|
|
354
|
+
advisorTool({
|
|
355
|
+
from: config.name,
|
|
356
|
+
consult: (q) => advisor.consult(q),
|
|
357
|
+
emit: (e) => discusser.loop.emitOrchestratorEvent(e),
|
|
358
|
+
budget,
|
|
359
|
+
model: advisorModel,
|
|
360
|
+
}),
|
|
361
|
+
];
|
|
362
|
+
}
|
|
363
|
+
|
|
310
364
|
const agentServer = createDiscussAgentToolServer(ctx, {
|
|
311
365
|
from: config.name,
|
|
366
|
+
...(extraTools && { extraTools }),
|
|
312
367
|
});
|
|
313
368
|
|
|
369
|
+
const emitAgentLine = (line) => discusser.loop.emitLine(config.name, line);
|
|
314
370
|
const runner = createAgentRunner({
|
|
315
371
|
cwd: config.cwd ?? resolvedLeadCwd,
|
|
316
372
|
query,
|
|
@@ -318,17 +374,16 @@ export function createDiscusser({
|
|
|
318
374
|
model: agentModel ?? AGENT_MODEL,
|
|
319
375
|
maxTurns: config.maxTurns ?? 50,
|
|
320
376
|
allowedTools: config.allowedTools,
|
|
321
|
-
onLine:
|
|
377
|
+
onLine: recorder
|
|
378
|
+
? (line) => {
|
|
379
|
+
emitAgentLine(line);
|
|
380
|
+
recorder.recordMessage(line);
|
|
381
|
+
}
|
|
382
|
+
: emitAgentLine,
|
|
383
|
+
...(recorder && { onPrompt: (text) => recorder.recordPrompt(text) }),
|
|
322
384
|
mcpServers: { orchestration: agentServer },
|
|
323
385
|
settingSources: ["project"],
|
|
324
|
-
systemPrompt
|
|
325
|
-
role: "agent",
|
|
326
|
-
profile: config.agentProfile,
|
|
327
|
-
profilesDir: resolvedProfilesDir,
|
|
328
|
-
trailer: DISCUSS_AGENT_SYSTEM_PROMPT,
|
|
329
|
-
amend: config.systemPromptAmend,
|
|
330
|
-
runtime,
|
|
331
|
-
}),
|
|
386
|
+
systemPrompt,
|
|
332
387
|
redactor,
|
|
333
388
|
});
|
|
334
389
|
|
package/src/facilitator.js
CHANGED
|
@@ -12,11 +12,18 @@ import { createAgentRunner } from "./agent-runner.js";
|
|
|
12
12
|
import { composeSystemPrompt } from "./profile-prompt.js";
|
|
13
13
|
import { createMessageBus } from "./message-bus.js";
|
|
14
14
|
import {
|
|
15
|
+
advisorTool,
|
|
15
16
|
createOrchestrationContext,
|
|
16
17
|
createFacilitatorToolServer,
|
|
17
18
|
createFacilitatedAgentToolServer,
|
|
18
19
|
} from "./orchestration-toolkit.js";
|
|
19
20
|
import { OrchestrationLoop } from "./orchestration-loop.js";
|
|
21
|
+
import {
|
|
22
|
+
createAdvisor,
|
|
23
|
+
createAdvisorBudget,
|
|
24
|
+
withAdvisorGuidance,
|
|
25
|
+
} from "./advisor.js";
|
|
26
|
+
import { createTranscriptRecorder } from "./transcript-recorder.js";
|
|
20
27
|
|
|
21
28
|
/** System prompt for the facilitator lead. L0 mechanics only per COALIGNED. */
|
|
22
29
|
export const FACILITATOR_SYSTEM_PROMPT =
|
|
@@ -92,6 +99,8 @@ const devNull = new Writable({
|
|
|
92
99
|
* @param {string} [deps.facilitatorProfile]
|
|
93
100
|
* @param {string} [deps.profilesDir]
|
|
94
101
|
* @param {string} [deps.taskAmend]
|
|
102
|
+
* @param {string} [deps.advisorModel] - Claude model for advisor consults; absent means no Advisor tool is offered.
|
|
103
|
+
* @param {number} [deps.advisorMaxUses] - Session-wide consult budget shared by all agent participants (default 3).
|
|
95
104
|
* @returns {Facilitator}
|
|
96
105
|
*/
|
|
97
106
|
export function createFacilitator({
|
|
@@ -110,6 +119,8 @@ export function createFacilitator({
|
|
|
110
119
|
taskAmend,
|
|
111
120
|
redactor,
|
|
112
121
|
runtime,
|
|
122
|
+
advisorModel,
|
|
123
|
+
advisorMaxUses,
|
|
113
124
|
}) {
|
|
114
125
|
if (!redactor) throw new Error("redactor is required");
|
|
115
126
|
if (!runtime) throw new Error("runtime is required");
|
|
@@ -129,11 +140,55 @@ export function createFacilitator({
|
|
|
129
140
|
|
|
130
141
|
const facilitatorServer = createFacilitatorToolServer(ctx);
|
|
131
142
|
|
|
143
|
+
const abortController = new AbortController();
|
|
144
|
+
// One budget per session, shared by every agent's Advisor handler.
|
|
145
|
+
const budget = advisorModel ? createAdvisorBudget(advisorMaxUses ?? 3) : null;
|
|
146
|
+
|
|
132
147
|
const agents = agentConfigs.map((config) => {
|
|
148
|
+
// Everything advisor-shaped is gated on advisorModel; with it unset the
|
|
149
|
+
// composed prompt and tool surface are byte-identical to today's.
|
|
150
|
+
const systemPrompt = composeSystemPrompt({
|
|
151
|
+
role: "agent",
|
|
152
|
+
profile: config.agentProfile,
|
|
153
|
+
profilesDir: resolvedProfilesDir,
|
|
154
|
+
trailer: FACILITATED_AGENT_SYSTEM_PROMPT,
|
|
155
|
+
amend: withAdvisorGuidance(config.systemPromptAmend, budget),
|
|
156
|
+
runtime,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
let recorder = null;
|
|
160
|
+
let extraTools;
|
|
161
|
+
if (advisorModel) {
|
|
162
|
+
recorder = createTranscriptRecorder({ systemPrompt, redactor });
|
|
163
|
+
// Late-bound through the `let facilitator` closure — the instance
|
|
164
|
+
// does not exist yet when the advisor and tool are built.
|
|
165
|
+
const advisor = createAdvisor({
|
|
166
|
+
model: advisorModel,
|
|
167
|
+
cwd: config.cwd ?? facilitatorCwd,
|
|
168
|
+
query,
|
|
169
|
+
recorder,
|
|
170
|
+
redactor,
|
|
171
|
+
runtime,
|
|
172
|
+
onLine: (line) => facilitator.emitLine("advisor", line),
|
|
173
|
+
});
|
|
174
|
+
abortController.signal.addEventListener("abort", () => advisor.abort());
|
|
175
|
+
extraTools = [
|
|
176
|
+
advisorTool({
|
|
177
|
+
from: config.name,
|
|
178
|
+
consult: (q) => advisor.consult(q),
|
|
179
|
+
emit: (e) => facilitator.emitOrchestratorEvent(e),
|
|
180
|
+
budget,
|
|
181
|
+
model: advisorModel,
|
|
182
|
+
}),
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
|
|
133
186
|
const agentServer = createFacilitatedAgentToolServer(ctx, {
|
|
134
187
|
from: config.name,
|
|
188
|
+
...(extraTools && { extraTools }),
|
|
135
189
|
});
|
|
136
190
|
|
|
191
|
+
const emitAgentLine = (line) => facilitator.emitLine(config.name, line);
|
|
137
192
|
const runner = createAgentRunner({
|
|
138
193
|
cwd: config.cwd ?? facilitatorCwd,
|
|
139
194
|
query,
|
|
@@ -141,17 +196,16 @@ export function createFacilitator({
|
|
|
141
196
|
model: agentModel ?? model,
|
|
142
197
|
maxTurns: config.maxTurns ?? 50,
|
|
143
198
|
allowedTools: config.allowedTools,
|
|
144
|
-
onLine:
|
|
199
|
+
onLine: recorder
|
|
200
|
+
? (line) => {
|
|
201
|
+
emitAgentLine(line);
|
|
202
|
+
recorder.recordMessage(line);
|
|
203
|
+
}
|
|
204
|
+
: emitAgentLine,
|
|
205
|
+
...(recorder && { onPrompt: (text) => recorder.recordPrompt(text) }),
|
|
145
206
|
mcpServers: { orchestration: agentServer },
|
|
146
207
|
settingSources: ["project"],
|
|
147
|
-
systemPrompt
|
|
148
|
-
role: "agent",
|
|
149
|
-
profile: config.agentProfile,
|
|
150
|
-
profilesDir: resolvedProfilesDir,
|
|
151
|
-
trailer: FACILITATED_AGENT_SYSTEM_PROMPT,
|
|
152
|
-
amend: config.systemPromptAmend,
|
|
153
|
-
runtime,
|
|
154
|
-
}),
|
|
208
|
+
systemPrompt,
|
|
155
209
|
redactor,
|
|
156
210
|
});
|
|
157
211
|
|
|
@@ -200,6 +254,7 @@ export function createFacilitator({
|
|
|
200
254
|
ctx,
|
|
201
255
|
taskAmend,
|
|
202
256
|
redactor,
|
|
257
|
+
abortController,
|
|
203
258
|
});
|
|
204
259
|
return facilitator;
|
|
205
260
|
}
|
package/src/index.js
CHANGED
|
@@ -26,6 +26,7 @@ export {
|
|
|
26
26
|
export { TeeWriter, createTeeWriter } from "./tee-writer.js";
|
|
27
27
|
export { SequenceCounter, createSequenceCounter } from "./sequence-counter.js";
|
|
28
28
|
export {
|
|
29
|
+
advisorTool,
|
|
29
30
|
createOrchestrationContext,
|
|
30
31
|
createRequestForCommentHandler,
|
|
31
32
|
createSupervisorToolServer,
|
|
@@ -34,6 +35,14 @@ export {
|
|
|
34
35
|
createFacilitatedAgentToolServer,
|
|
35
36
|
createJudgeToolServer,
|
|
36
37
|
} from "./orchestration-toolkit.js";
|
|
38
|
+
export {
|
|
39
|
+
ADVISOR_SYSTEM_PROMPT,
|
|
40
|
+
advisorGuidance,
|
|
41
|
+
createAdvisor,
|
|
42
|
+
createAdvisorBudget,
|
|
43
|
+
DEFAULT_CONSULT_TIMEOUT_MS,
|
|
44
|
+
} from "./advisor.js";
|
|
45
|
+
export { createTranscriptRecorder } from "./transcript-recorder.js";
|
|
37
46
|
export { MessageBus, createMessageBus } from "./message-bus.js";
|
|
38
47
|
export { OrchestrationLoop } from "./orchestration-loop.js";
|
|
39
48
|
export {
|
|
@@ -337,6 +337,59 @@ function concludeTool(ctx) {
|
|
|
337
337
|
);
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
const ADVISOR_DESC =
|
|
341
|
+
"Consult a stronger model on one focused question. Your full session context (system prompt, prompts, transcript so far) is forwarded automatically — you cannot restrict it. The advice returns in the tool result. The consult budget is shared session-wide across all participants.";
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Build the `Advisor` consult tool for one caller. Mode-agnostic: loop
|
|
345
|
+
* modes pass it into the agent tool-server factories via `extraTools`;
|
|
346
|
+
* run mode gives it a dedicated server. No orchestration-context
|
|
347
|
+
* dependency — the budget object and emit callback are injected.
|
|
348
|
+
*
|
|
349
|
+
* @param {object} deps
|
|
350
|
+
* @param {string} deps.from - Caller's canonical name (event attribution).
|
|
351
|
+
* @param {(question: string) => Promise<{advice?: string, unavailable?: boolean, reason?: string, durationMs: number}>} deps.consult
|
|
352
|
+
* @param {(event: object) => void} deps.emit - Orchestrator-event emitter for the `advisor_consult` event.
|
|
353
|
+
* @param {{maxUses: number, used: number}} deps.budget - Session-wide budget shared by every caller's handler.
|
|
354
|
+
* @param {string} deps.model - Advisor model id, carried on the consult event.
|
|
355
|
+
*/
|
|
356
|
+
export function advisorTool({ from, consult, emit, budget, model }) {
|
|
357
|
+
return tool(
|
|
358
|
+
"Advisor",
|
|
359
|
+
ADVISOR_DESC,
|
|
360
|
+
{ question: z.string() },
|
|
361
|
+
async ({ question }) => {
|
|
362
|
+
if (budget.used >= budget.maxUses) {
|
|
363
|
+
return textResult(
|
|
364
|
+
`Consult limit reached (${budget.maxUses}/${budget.maxUses} used) — proceed with your best judgment.`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
// Increment before the first await so two concurrent callers cannot
|
|
368
|
+
// both pass a last-slot check.
|
|
369
|
+
budget.used++;
|
|
370
|
+
const r = await consult(question);
|
|
371
|
+
const remaining = budget.maxUses - budget.used;
|
|
372
|
+
emit({
|
|
373
|
+
type: "advisor_consult",
|
|
374
|
+
caller: from,
|
|
375
|
+
question,
|
|
376
|
+
model,
|
|
377
|
+
durationMs: r.durationMs,
|
|
378
|
+
remaining,
|
|
379
|
+
});
|
|
380
|
+
if (r.unavailable) {
|
|
381
|
+
// Not isError: fail-open, the caller continues normally.
|
|
382
|
+
return textResult(
|
|
383
|
+
`The advisor is unavailable (${r.reason}) — proceed with your best judgment.`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return textResult(
|
|
387
|
+
`${r.advice}\n\n[advisor consults remaining: ${remaining}]`,
|
|
388
|
+
);
|
|
389
|
+
},
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
340
393
|
const orchestrationServer = (tools) =>
|
|
341
394
|
createSdkMcpServer({ name: "orchestration", tools });
|
|
342
395
|
|
|
@@ -354,15 +407,16 @@ export function createSupervisorToolServer(ctx) {
|
|
|
354
407
|
]);
|
|
355
408
|
}
|
|
356
409
|
|
|
357
|
-
/** Supervised agent tools: Ask + Answer + Announce + RollCall. */
|
|
358
|
-
export function createSupervisedAgentToolServer(ctx) {
|
|
359
|
-
return orchestrationServer(
|
|
360
|
-
baseTools(ctx, {
|
|
410
|
+
/** Supervised agent tools: Ask + Answer + Announce + RollCall (+ extras). */
|
|
411
|
+
export function createSupervisedAgentToolServer(ctx, { extraTools = [] } = {}) {
|
|
412
|
+
return orchestrationServer([
|
|
413
|
+
...baseTools(ctx, {
|
|
361
414
|
from: "agent",
|
|
362
415
|
defaultTo: "supervisor",
|
|
363
416
|
broadcast: false,
|
|
364
417
|
}),
|
|
365
|
-
|
|
418
|
+
...extraTools,
|
|
419
|
+
]);
|
|
366
420
|
}
|
|
367
421
|
|
|
368
422
|
/** Facilitator tools: Ask + Answer + Announce + RollCall + Conclude. */
|
|
@@ -377,11 +431,15 @@ export function createFacilitatorToolServer(ctx) {
|
|
|
377
431
|
]);
|
|
378
432
|
}
|
|
379
433
|
|
|
380
|
-
/** Facilitated agent tools: Ask + Answer + Announce + RollCall + RequestForComment. */
|
|
381
|
-
export function createFacilitatedAgentToolServer(
|
|
434
|
+
/** Facilitated agent tools: Ask + Answer + Announce + RollCall + RequestForComment (+ extras). */
|
|
435
|
+
export function createFacilitatedAgentToolServer(
|
|
436
|
+
ctx,
|
|
437
|
+
{ from, extraTools = [] },
|
|
438
|
+
) {
|
|
382
439
|
return orchestrationServer([
|
|
383
440
|
...baseTools(ctx, { from, defaultTo: "facilitator", broadcast: true }),
|
|
384
441
|
requestForCommentTool(ctx),
|
|
442
|
+
...extraTools,
|
|
385
443
|
]);
|
|
386
444
|
}
|
|
387
445
|
|