osborn 0.9.143 → 0.9.145
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/dist/claude-llm.d.ts +13 -0
- package/dist/claude-llm.js +132 -33
- package/package.json +1 -1
package/dist/claude-llm.d.ts
CHANGED
|
@@ -231,6 +231,19 @@ export declare class ClaudeLLM extends llm.LLM {
|
|
|
231
231
|
onCheckpoint: (checkpointId: string) => void;
|
|
232
232
|
eventEmitter: EventEmitter;
|
|
233
233
|
}): void;
|
|
234
|
+
/**
|
|
235
|
+
* Dispatcher v1 — auto-spawn a reviewer after a writer sub-agent completes.
|
|
236
|
+
* Runs a one-shot query() with the reviewer agent and emits dispatch_rejected
|
|
237
|
+
* if the verdict is REJECT. A reviewer failure must never crash the consumer.
|
|
238
|
+
* Public so ClaudeLLMStream can call it via this.#llmRef.spawnReviewer().
|
|
239
|
+
*/
|
|
240
|
+
spawnReviewer(agentId: string, writerOutput: string, emitter: EventEmitter): Promise<void>;
|
|
241
|
+
/**
|
|
242
|
+
* Dispatcher v1 — research gate: vet a researcher sub-agent's output before
|
|
243
|
+
* it reaches the main agent. Emits dispatch_rejected with verdict 'NEEDS-MORE'
|
|
244
|
+
* (distinct from reviewer's 'REJECT') so the frontend can tell them apart.
|
|
245
|
+
*/
|
|
246
|
+
spawnResearchGate(agentId: string, researchOutput: string, emitter: EventEmitter): Promise<void>;
|
|
234
247
|
chat({ chatCtx, toolCtx, connOptions, abortController, }: {
|
|
235
248
|
chatCtx: llm.ChatContext;
|
|
236
249
|
toolCtx?: llm.ToolContext;
|
package/dist/claude-llm.js
CHANGED
|
@@ -555,9 +555,9 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
555
555
|
// Active queries — multiple can be running (SDK queues them internally).
|
|
556
556
|
// We keep ALL references so interrupt() can stop whatever is currently executing.
|
|
557
557
|
#activeQueries = new Set();
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
#
|
|
558
|
+
// Dedup guard — prevents double-firing reviewer/gate if SubagentStop fires
|
|
559
|
+
// more than once for the same agent_id (e.g. retry edge cases).
|
|
560
|
+
#dispatchedFor = new Set();
|
|
561
561
|
constructor(opts = {}) {
|
|
562
562
|
super();
|
|
563
563
|
// Session resume/continue options
|
|
@@ -970,6 +970,7 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
970
970
|
this.#persistentQuery = null;
|
|
971
971
|
this.#messageChannel = null;
|
|
972
972
|
this.#backgroundConsumerRunning = false;
|
|
973
|
+
this.#dispatchedFor.clear();
|
|
973
974
|
console.log('🔒 Persistent session closed');
|
|
974
975
|
}
|
|
975
976
|
/**
|
|
@@ -1092,29 +1093,6 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1092
1093
|
callbacks.eventEmitter.emit('tts_say', { text: ttsChunk });
|
|
1093
1094
|
}
|
|
1094
1095
|
}
|
|
1095
|
-
// Dispatcher v1 — record Task tool_use blocks so we can correlate
|
|
1096
|
-
// the matching task_summary message to the subagent type.
|
|
1097
|
-
if (block.type === 'tool_use' && block.name === 'Task') {
|
|
1098
|
-
console.log('[DISPATCH-PROBE] task tool_use', JSON.stringify({ id: block.id, subagent_type: block.input?.subagent_type }));
|
|
1099
|
-
this.#dispatchAgentTypes.set(block.id, block.input?.subagent_type);
|
|
1100
|
-
statusManager.upsertDispatch(block.id, {
|
|
1101
|
-
owner: 'orchestrator',
|
|
1102
|
-
subagentType: block.input?.subagent_type,
|
|
1103
|
-
dispatchState: 'running',
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
// Dispatcher v1 — catch sub-agent completion signals
|
|
1109
|
-
if (msg.type === 'system' && msg.subtype === 'task_summary') {
|
|
1110
|
-
console.log('[DISPATCH-PROBE] task_summary raw:', JSON.stringify(msg).slice(0, 500));
|
|
1111
|
-
const tuid = msg.tool_use_id ?? msg.toolUseId;
|
|
1112
|
-
const output = msg.summary ?? msg.result ?? msg.output ?? '';
|
|
1113
|
-
const subType = tuid ? this.#dispatchAgentTypes.get(tuid) : undefined;
|
|
1114
|
-
statusManager.upsertDispatch(tuid ?? `ts-${Date.now()}`, { subagentType: subType, dispatchState: 'completed', artifact: String(output) });
|
|
1115
|
-
console.log(`[DISPATCH] completed type=${subType ?? '?'} tuid=${(tuid ?? '?').slice(0, 8)} len=${String(output).length}`);
|
|
1116
|
-
if (subType === 'writer' && output) {
|
|
1117
|
-
this.#spawnReviewer(tuid, String(output), callbacks.eventEmitter);
|
|
1118
1096
|
}
|
|
1119
1097
|
}
|
|
1120
1098
|
// Result — marks end of a turn (but we keep consuming for next turn)
|
|
@@ -1147,8 +1125,13 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1147
1125
|
* Dispatcher v1 — auto-spawn a reviewer after a writer sub-agent completes.
|
|
1148
1126
|
* Runs a one-shot query() with the reviewer agent and emits dispatch_rejected
|
|
1149
1127
|
* if the verdict is REJECT. A reviewer failure must never crash the consumer.
|
|
1128
|
+
* Public so ClaudeLLMStream can call it via this.#llmRef.spawnReviewer().
|
|
1150
1129
|
*/
|
|
1151
|
-
async
|
|
1130
|
+
async spawnReviewer(agentId, writerOutput, emitter) {
|
|
1131
|
+
// Dedup guard — SubagentStop may fire more than once for the same agent_id.
|
|
1132
|
+
if (this.#dispatchedFor.has(agentId))
|
|
1133
|
+
return;
|
|
1134
|
+
this.#dispatchedFor.add(agentId);
|
|
1152
1135
|
try {
|
|
1153
1136
|
const prompt = [
|
|
1154
1137
|
'Use the reviewer sub-agent to review this writer output for correctness/spec-adherence/obvious bugs.',
|
|
@@ -1158,12 +1141,36 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1158
1141
|
writerOutput.slice(0, 8000),
|
|
1159
1142
|
'</writer_output>',
|
|
1160
1143
|
].join('\n');
|
|
1144
|
+
// Do NOT pass agents here — the reviewer must be review-only and must not
|
|
1145
|
+
// be able to spawn writer/researcher/reasoner sub-agents. Passing an empty
|
|
1146
|
+
// agents roster prevents any SubagentStop(agent_type==='writer') from
|
|
1147
|
+
// firing inside this one-shot query and re-arming the backstop loop.
|
|
1161
1148
|
const reviewerOptions = {
|
|
1162
1149
|
cwd: this.#opts.workingDirectory,
|
|
1163
1150
|
permissionMode: 'default',
|
|
1164
|
-
|
|
1151
|
+
hooks: {
|
|
1152
|
+
PreToolUse: [{
|
|
1153
|
+
matcher: '.*',
|
|
1154
|
+
hooks: [async (input) => {
|
|
1155
|
+
const toolName = input?.tool_name || 'unknown';
|
|
1156
|
+
const toolInput = input?.tool_input || {};
|
|
1157
|
+
emitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: 'reviewer' });
|
|
1158
|
+
return {};
|
|
1159
|
+
}],
|
|
1160
|
+
}],
|
|
1161
|
+
PostToolUse: [{
|
|
1162
|
+
matcher: '.*',
|
|
1163
|
+
hooks: [async (input) => {
|
|
1164
|
+
const toolName = input?.tool_name || 'unknown';
|
|
1165
|
+
const toolInput = input?.tool_input || {};
|
|
1166
|
+
const toolResponse = input?.tool_response;
|
|
1167
|
+
emitter.emit('tool_result', { name: toolName, input: toolInput, response: toolResponse, agentRole: 'reviewer' });
|
|
1168
|
+
return {};
|
|
1169
|
+
}],
|
|
1170
|
+
}],
|
|
1171
|
+
},
|
|
1165
1172
|
};
|
|
1166
|
-
console.log(`[DISPATCH] spawning reviewer for
|
|
1173
|
+
console.log(`[DISPATCH] spawning reviewer for agentId=${agentId.slice(0, 8)}`);
|
|
1167
1174
|
const reviewerQuery = query({ prompt, options: reviewerOptions });
|
|
1168
1175
|
this.#activeQueries.add(reviewerQuery);
|
|
1169
1176
|
let reviewerText = '';
|
|
@@ -1181,18 +1188,97 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1181
1188
|
const verdictMatch = reviewerText.match(/VERDICT:\s*(ACCEPT|REJECT)/i);
|
|
1182
1189
|
const verdict = verdictMatch ? verdictMatch[1].toUpperCase() : null;
|
|
1183
1190
|
if (verdict === 'REJECT') {
|
|
1184
|
-
console.log(`[DISPATCH] review REJECT for
|
|
1185
|
-
statusManager.upsertDispatch(
|
|
1186
|
-
emitter.emit('dispatch_rejected', { tuid, verdict: 'REJECT', review: reviewerText });
|
|
1191
|
+
console.log(`[DISPATCH] review REJECT for agentId=${agentId.slice(0, 8)}`);
|
|
1192
|
+
statusManager.upsertDispatch(agentId, { dispatchState: 'rejected', artifact: reviewerText });
|
|
1193
|
+
emitter.emit('dispatch_rejected', { tuid: agentId, verdict: 'REJECT', review: reviewerText });
|
|
1187
1194
|
}
|
|
1188
1195
|
else {
|
|
1189
|
-
console.log(`[DISPATCH] review ACCEPT for
|
|
1196
|
+
console.log(`[DISPATCH] review ACCEPT for agentId=${agentId.slice(0, 8)}`);
|
|
1190
1197
|
}
|
|
1191
1198
|
}
|
|
1192
1199
|
catch (err) {
|
|
1193
1200
|
console.error('[DISPATCH] reviewer spawn failed (non-fatal):', err);
|
|
1194
1201
|
}
|
|
1195
1202
|
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Dispatcher v1 — research gate: vet a researcher sub-agent's output before
|
|
1205
|
+
* it reaches the main agent. Emits dispatch_rejected with verdict 'NEEDS-MORE'
|
|
1206
|
+
* (distinct from reviewer's 'REJECT') so the frontend can tell them apart.
|
|
1207
|
+
*/
|
|
1208
|
+
async spawnResearchGate(agentId, researchOutput, emitter) {
|
|
1209
|
+
// Dedup guard — SubagentStop may fire more than once for the same agent_id.
|
|
1210
|
+
if (this.#dispatchedFor.has(agentId))
|
|
1211
|
+
return;
|
|
1212
|
+
this.#dispatchedFor.add(agentId);
|
|
1213
|
+
try {
|
|
1214
|
+
const prompt = [
|
|
1215
|
+
'Use the reasoner sub-agent to vet this research output.',
|
|
1216
|
+
'Determine whether the research is sufficient to answer the original question.',
|
|
1217
|
+
'End your reply with exactly `GATE: PASS` or `GATE: NEEDS-MORE`.',
|
|
1218
|
+
'',
|
|
1219
|
+
'<research_output>',
|
|
1220
|
+
researchOutput.slice(0, 8000),
|
|
1221
|
+
'</research_output>',
|
|
1222
|
+
].join('\n');
|
|
1223
|
+
// Do NOT pass agents here — the research-gate reasoner must be review-only
|
|
1224
|
+
// and must not be able to spawn sub-agents. Same rationale as spawnReviewer:
|
|
1225
|
+
// an agents roster would allow delegation back to the writer, which would
|
|
1226
|
+
// fire SubagentStop(agent_type==='writer') and re-arm the backstop.
|
|
1227
|
+
const gateOptions = {
|
|
1228
|
+
cwd: this.#opts.workingDirectory,
|
|
1229
|
+
permissionMode: 'default',
|
|
1230
|
+
hooks: {
|
|
1231
|
+
PreToolUse: [{
|
|
1232
|
+
matcher: '.*',
|
|
1233
|
+
hooks: [async (input) => {
|
|
1234
|
+
const toolName = input?.tool_name || 'unknown';
|
|
1235
|
+
const toolInput = input?.tool_input || {};
|
|
1236
|
+
emitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: 'reasoner' });
|
|
1237
|
+
return {};
|
|
1238
|
+
}],
|
|
1239
|
+
}],
|
|
1240
|
+
PostToolUse: [{
|
|
1241
|
+
matcher: '.*',
|
|
1242
|
+
hooks: [async (input) => {
|
|
1243
|
+
const toolName = input?.tool_name || 'unknown';
|
|
1244
|
+
const toolInput = input?.tool_input || {};
|
|
1245
|
+
const toolResponse = input?.tool_response;
|
|
1246
|
+
emitter.emit('tool_result', { name: toolName, input: toolInput, response: toolResponse, agentRole: 'reasoner' });
|
|
1247
|
+
return {};
|
|
1248
|
+
}],
|
|
1249
|
+
}],
|
|
1250
|
+
},
|
|
1251
|
+
};
|
|
1252
|
+
console.log(`[DISPATCH] spawning research-gate for agentId=${agentId.slice(0, 8)}`);
|
|
1253
|
+
const gateQuery = query({ prompt, options: gateOptions });
|
|
1254
|
+
this.#activeQueries.add(gateQuery);
|
|
1255
|
+
let review = '';
|
|
1256
|
+
try {
|
|
1257
|
+
for await (const msg of gateQuery) {
|
|
1258
|
+
const m = msg;
|
|
1259
|
+
if (m.type === 'result' && m.result) {
|
|
1260
|
+
review = String(m.result);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
finally {
|
|
1265
|
+
this.#activeQueries.delete(gateQuery);
|
|
1266
|
+
}
|
|
1267
|
+
const gateMatch = review.match(/GATE:\s*(PASS|NEEDS-MORE)/i);
|
|
1268
|
+
const gateVerdict = gateMatch ? gateMatch[1].toUpperCase() : null;
|
|
1269
|
+
if (gateVerdict === 'NEEDS-MORE') {
|
|
1270
|
+
console.log(`[DISPATCH] research-gate NEEDS-MORE for agentId=${agentId.slice(0, 8)}`);
|
|
1271
|
+
statusManager.upsertDispatch(agentId, { dispatchState: 'rejected', artifact: review });
|
|
1272
|
+
emitter.emit('dispatch_rejected', { tuid: agentId, verdict: 'NEEDS-MORE', review });
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
console.log(`[DISPATCH] research-gate PASS for agentId=${agentId.slice(0, 8)}`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
catch (err) {
|
|
1279
|
+
console.error('[DISPATCH] research-gate spawn failed (non-fatal):', err);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1196
1282
|
chat({ chatCtx, toolCtx, connOptions = DEFAULT_API_CONNECT_OPTIONS, abortController, }) {
|
|
1197
1283
|
return new ClaudeLLMStream(this, {
|
|
1198
1284
|
chatCtx,
|
|
@@ -1670,6 +1756,19 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1670
1756
|
matcher: '.*',
|
|
1671
1757
|
hooks: [async (input) => {
|
|
1672
1758
|
console.log('[LIFECYCLE-PROBE] SubagentStop', JSON.stringify(input));
|
|
1759
|
+
const at = input?.agent_type;
|
|
1760
|
+
const msg = String(input?.last_assistant_message ?? '');
|
|
1761
|
+
const aid = input?.agent_id ?? ('sa-' + Date.now());
|
|
1762
|
+
statusManager.upsertDispatch(aid, { subagentType: at, dispatchState: 'completed', artifact: msg });
|
|
1763
|
+
// Infinite-loop guard — never re-dispatch the reviewer or reasoner.
|
|
1764
|
+
if (at === 'reviewer' || at === 'reasoner')
|
|
1765
|
+
return {};
|
|
1766
|
+
if (at === 'writer' && msg) {
|
|
1767
|
+
void this.#llmRef.spawnReviewer(aid, msg, this.#eventEmitter);
|
|
1768
|
+
}
|
|
1769
|
+
else if (at === 'researcher' && msg) {
|
|
1770
|
+
void this.#llmRef.spawnResearchGate(aid, msg, this.#eventEmitter);
|
|
1771
|
+
}
|
|
1673
1772
|
return {};
|
|
1674
1773
|
}]
|
|
1675
1774
|
}],
|