osborn 0.9.141 → 0.9.142
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.js +77 -0
- package/dist/index.js +10 -0
- package/dist/status-manager.d.ts +15 -0
- package/dist/status-manager.js +42 -0
- package/package.json +1 -1
package/dist/claude-llm.js
CHANGED
|
@@ -10,6 +10,7 @@ import { llm, shortuuid, DEFAULT_API_CONNECT_OPTIONS } from '@livekit/agents';
|
|
|
10
10
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
11
11
|
import { EventEmitter } from 'events';
|
|
12
12
|
import { saveSessionMetadata, getSessionWorkspace } from './config.js';
|
|
13
|
+
import { statusManager } from './status-manager.js';
|
|
13
14
|
import { getResearchSystemPrompt, getDirectModeResearchPrompt } from './prompts.js';
|
|
14
15
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
15
16
|
import { join, dirname } from 'node:path';
|
|
@@ -505,6 +506,9 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
505
506
|
// Active queries — multiple can be running (SDK queues them internally).
|
|
506
507
|
// We keep ALL references so interrupt() can stop whatever is currently executing.
|
|
507
508
|
#activeQueries = new Set();
|
|
509
|
+
// Dispatcher v1 — maps tool_use_id → subagent_type for Task tool_use blocks
|
|
510
|
+
// so that when task_summary fires we know which agent type just finished.
|
|
511
|
+
#dispatchAgentTypes = new Map();
|
|
508
512
|
constructor(opts = {}) {
|
|
509
513
|
super();
|
|
510
514
|
// Session resume/continue options
|
|
@@ -1039,6 +1043,29 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1039
1043
|
callbacks.eventEmitter.emit('tts_say', { text: ttsChunk });
|
|
1040
1044
|
}
|
|
1041
1045
|
}
|
|
1046
|
+
// Dispatcher v1 — record Task tool_use blocks so we can correlate
|
|
1047
|
+
// the matching task_summary message to the subagent type.
|
|
1048
|
+
if (block.type === 'tool_use' && block.name === 'Task') {
|
|
1049
|
+
console.log('[DISPATCH-PROBE] task tool_use', JSON.stringify({ id: block.id, subagent_type: block.input?.subagent_type }));
|
|
1050
|
+
this.#dispatchAgentTypes.set(block.id, block.input?.subagent_type);
|
|
1051
|
+
statusManager.upsertDispatch(block.id, {
|
|
1052
|
+
owner: 'orchestrator',
|
|
1053
|
+
subagentType: block.input?.subagent_type,
|
|
1054
|
+
dispatchState: 'running',
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
// Dispatcher v1 — catch sub-agent completion signals
|
|
1060
|
+
if (msg.type === 'system' && msg.subtype === 'task_summary') {
|
|
1061
|
+
console.log('[DISPATCH-PROBE] task_summary raw:', JSON.stringify(msg).slice(0, 500));
|
|
1062
|
+
const tuid = msg.tool_use_id ?? msg.toolUseId;
|
|
1063
|
+
const output = msg.summary ?? msg.result ?? msg.output ?? '';
|
|
1064
|
+
const subType = tuid ? this.#dispatchAgentTypes.get(tuid) : undefined;
|
|
1065
|
+
statusManager.upsertDispatch(tuid ?? `ts-${Date.now()}`, { subagentType: subType, dispatchState: 'completed', artifact: String(output) });
|
|
1066
|
+
console.log(`[DISPATCH] completed type=${subType ?? '?'} tuid=${(tuid ?? '?').slice(0, 8)} len=${String(output).length}`);
|
|
1067
|
+
if (subType === 'writer' && output) {
|
|
1068
|
+
this.#spawnReviewer(tuid, String(output), callbacks.eventEmitter);
|
|
1042
1069
|
}
|
|
1043
1070
|
}
|
|
1044
1071
|
// Result — marks end of a turn (but we keep consuming for next turn)
|
|
@@ -1067,6 +1094,56 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1067
1094
|
console.log('🔒 Persistent session background consumer exited');
|
|
1068
1095
|
}
|
|
1069
1096
|
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Dispatcher v1 — auto-spawn a reviewer after a writer sub-agent completes.
|
|
1099
|
+
* Runs a one-shot query() with the reviewer agent and emits dispatch_rejected
|
|
1100
|
+
* if the verdict is REJECT. A reviewer failure must never crash the consumer.
|
|
1101
|
+
*/
|
|
1102
|
+
async #spawnReviewer(tuid, writerOutput, emitter) {
|
|
1103
|
+
try {
|
|
1104
|
+
const prompt = [
|
|
1105
|
+
'Use the reviewer sub-agent to review this writer output for correctness/spec-adherence/obvious bugs.',
|
|
1106
|
+
'End your reply with exactly `VERDICT: ACCEPT` or `VERDICT: REJECT`.',
|
|
1107
|
+
'',
|
|
1108
|
+
'<writer_output>',
|
|
1109
|
+
writerOutput.slice(0, 8000),
|
|
1110
|
+
'</writer_output>',
|
|
1111
|
+
].join('\n');
|
|
1112
|
+
const reviewerOptions = {
|
|
1113
|
+
cwd: this.#opts.workingDirectory,
|
|
1114
|
+
permissionMode: 'default',
|
|
1115
|
+
agents: NAMED_AGENTS,
|
|
1116
|
+
};
|
|
1117
|
+
console.log(`[DISPATCH] spawning reviewer for tuid=${tuid.slice(0, 8)}`);
|
|
1118
|
+
const reviewerQuery = query({ prompt, options: reviewerOptions });
|
|
1119
|
+
this.#activeQueries.add(reviewerQuery);
|
|
1120
|
+
let reviewerText = '';
|
|
1121
|
+
try {
|
|
1122
|
+
for await (const msg of reviewerQuery) {
|
|
1123
|
+
const m = msg;
|
|
1124
|
+
if (m.type === 'result' && m.result) {
|
|
1125
|
+
reviewerText = String(m.result);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
finally {
|
|
1130
|
+
this.#activeQueries.delete(reviewerQuery);
|
|
1131
|
+
}
|
|
1132
|
+
const verdictMatch = reviewerText.match(/VERDICT:\s*(ACCEPT|REJECT)/i);
|
|
1133
|
+
const verdict = verdictMatch ? verdictMatch[1].toUpperCase() : null;
|
|
1134
|
+
if (verdict === 'REJECT') {
|
|
1135
|
+
console.log(`[DISPATCH] review REJECT for tuid=${tuid.slice(0, 8)}`);
|
|
1136
|
+
statusManager.upsertDispatch(tuid, { dispatchState: 'rejected', artifact: reviewerText });
|
|
1137
|
+
emitter.emit('dispatch_rejected', { tuid, verdict: 'REJECT', review: reviewerText });
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
console.log(`[DISPATCH] review ACCEPT for tuid=${tuid.slice(0, 8)}`);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
catch (err) {
|
|
1144
|
+
console.error('[DISPATCH] reviewer spawn failed (non-fatal):', err);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1070
1147
|
chat({ chatCtx, toolCtx, connOptions = DEFAULT_API_CONNECT_OPTIONS, abortController, }) {
|
|
1071
1148
|
return new ClaudeLLMStream(this, {
|
|
1072
1149
|
chatCtx,
|
package/dist/index.js
CHANGED
|
@@ -3086,6 +3086,16 @@ async function main() {
|
|
|
3086
3086
|
checkpointId: data.checkpointId,
|
|
3087
3087
|
});
|
|
3088
3088
|
});
|
|
3089
|
+
// Dispatcher v1 — writer reviewer rejected verdict → frontend
|
|
3090
|
+
directLLM.events.on('dispatch_rejected', (data) => {
|
|
3091
|
+
console.log(`[DISPATCH] dispatch_rejected tuid=${(data.tuid ?? '').slice(0, 8)} sending to frontend`);
|
|
3092
|
+
sendToFrontend({
|
|
3093
|
+
type: 'task_review',
|
|
3094
|
+
tuid: data.tuid,
|
|
3095
|
+
verdict: data.verdict,
|
|
3096
|
+
review: data.review,
|
|
3097
|
+
});
|
|
3098
|
+
});
|
|
3089
3099
|
// Create the Agent with instructions, STT, LLM, TTS
|
|
3090
3100
|
// VAD (Silero ONNX) removed — caused 2-5s inference lag on CPU, making interruption detection worse
|
|
3091
3101
|
// Turn detection is server-side (Deepgram endpointing), interruptions handled by STT
|
package/dist/status-manager.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface TaskStatus {
|
|
|
18
18
|
progress?: number;
|
|
19
19
|
progressUpdates: string[];
|
|
20
20
|
lastUpdate?: string;
|
|
21
|
+
owner?: string;
|
|
22
|
+
subagentType?: string;
|
|
23
|
+
dispatchState?: 'pending' | 'running' | 'completed' | 'rejected' | 'failed';
|
|
24
|
+
artifact?: string;
|
|
21
25
|
}
|
|
22
26
|
export interface StatusUpdate {
|
|
23
27
|
hasUpdates: boolean;
|
|
@@ -86,5 +90,16 @@ export declare class StatusManager {
|
|
|
86
90
|
* Get context summary for the brain
|
|
87
91
|
*/
|
|
88
92
|
getContextSummary(): string;
|
|
93
|
+
/**
|
|
94
|
+
* Dispatcher v1 — create or update a dispatch entry keyed by tool_use_id.
|
|
95
|
+
* Creates a minimal TaskStatus shell if the id doesn't yet exist so callers
|
|
96
|
+
* can upsert without a prior registerTask() call.
|
|
97
|
+
*/
|
|
98
|
+
upsertDispatch(id: string, fields: {
|
|
99
|
+
owner?: string;
|
|
100
|
+
subagentType?: string;
|
|
101
|
+
dispatchState?: TaskStatus['dispatchState'];
|
|
102
|
+
artifact?: string;
|
|
103
|
+
}): void;
|
|
89
104
|
}
|
|
90
105
|
export declare const statusManager: StatusManager;
|
package/dist/status-manager.js
CHANGED
|
@@ -182,6 +182,48 @@ export class StatusManager {
|
|
|
182
182
|
getContextSummary() {
|
|
183
183
|
return this.conversationContext.slice(-5).join(' | ');
|
|
184
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Dispatcher v1 — create or update a dispatch entry keyed by tool_use_id.
|
|
187
|
+
* Creates a minimal TaskStatus shell if the id doesn't yet exist so callers
|
|
188
|
+
* can upsert without a prior registerTask() call.
|
|
189
|
+
*/
|
|
190
|
+
upsertDispatch(id, fields) {
|
|
191
|
+
if (!this.tasks.has(id)) {
|
|
192
|
+
this.tasks.set(id, {
|
|
193
|
+
id,
|
|
194
|
+
type: 'execute',
|
|
195
|
+
// Use an empty query so narration never speaks a bogus "dispatch:<id>" string.
|
|
196
|
+
// Dispatch progress is tracked exclusively via dispatchState; status starts as
|
|
197
|
+
// 'pending' (not 'running') so getStatusUpdate() runningTasks narration is skipped.
|
|
198
|
+
query: '',
|
|
199
|
+
status: 'pending',
|
|
200
|
+
startedAt: Date.now(),
|
|
201
|
+
progressUpdates: [],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const task = this.tasks.get(id);
|
|
205
|
+
if (fields.owner !== undefined)
|
|
206
|
+
task.owner = fields.owner;
|
|
207
|
+
if (fields.subagentType !== undefined)
|
|
208
|
+
task.subagentType = fields.subagentType;
|
|
209
|
+
if (fields.dispatchState !== undefined)
|
|
210
|
+
task.dispatchState = fields.dispatchState;
|
|
211
|
+
if (fields.artifact !== undefined)
|
|
212
|
+
task.artifact = fields.artifact;
|
|
213
|
+
// Mirror dispatchState into the base status field so existing getStatusUpdate() callers see it.
|
|
214
|
+
// 'rejected' is also terminal — map it to 'failed' so the GC and readers treat it as finished.
|
|
215
|
+
if (fields.dispatchState === 'completed')
|
|
216
|
+
task.status = 'completed';
|
|
217
|
+
if (fields.dispatchState === 'failed' || fields.dispatchState === 'rejected')
|
|
218
|
+
task.status = 'failed';
|
|
219
|
+
// Fix: set completedAt when entering any terminal state so the task is GC-eligible via
|
|
220
|
+
// clearReportedTasks() and correctly surfaces in hasCompletedTasks() / getStatusUpdate().
|
|
221
|
+
// Without this the tasks Map grows unbounded (memory leak).
|
|
222
|
+
const isTerminal = task.status === 'completed' || task.status === 'failed';
|
|
223
|
+
if (isTerminal && !task.completedAt)
|
|
224
|
+
task.completedAt = Date.now();
|
|
225
|
+
console.log(`[Dispatch] upsertDispatch id=${id.slice(0, 8)} state=${fields.dispatchState ?? '-'} type=${fields.subagentType ?? '-'}`);
|
|
226
|
+
}
|
|
185
227
|
}
|
|
186
228
|
// Singleton instance
|
|
187
229
|
export const statusManager = new StatusManager();
|