osborn 0.9.245 → 0.9.247

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.
@@ -1817,6 +1817,10 @@ class ClaudeLLMStream extends llm.LLMStream {
1817
1817
  #abortController;
1818
1818
  #llmRef;
1819
1819
  #approvedWriterToolUseIds = new Set();
1820
+ // Last Task/Agent dispatch description per subagent_type, captured at PreToolUse so
1821
+ // SubagentStart can label the Running row with WHAT the sub-agent is doing even if the
1822
+ // SDK hook input omits the prompt. Cleared when consumed by the matching start.
1823
+ #pendingTaskByType = new Map();
1820
1824
  constructor(llmInstance, { chatCtx, toolCtx, connOptions, opts, sessionId, onSessionId, eventEmitter, onCheckpoint, onPermissionRequest, abortController, }) {
1821
1825
  super(llmInstance, { chatCtx, toolCtx, connOptions });
1822
1826
  this.#llmRef = llmInstance;
@@ -2026,6 +2030,14 @@ class ClaudeLLMStream extends llm.LLMStream {
2026
2030
  // tool_use, returns {} → canUseTool workspace auto-approve).
2027
2031
  }
2028
2032
  console.log(`🔧 Claude: ${toolName}`);
2033
+ // Capture the dispatch description/prompt per subagent_type so SubagentStart
2034
+ // can show WHAT a sub-agent is working on (see #pendingTaskByType).
2035
+ if (toolName === 'Task' || toolName === 'Agent') {
2036
+ const st = String(toolInput?.subagent_type || toolInput?.agent_type || '');
2037
+ const d = String(toolInput?.description || toolInput?.prompt || '');
2038
+ if (st && d)
2039
+ this.#pendingTaskByType.set(st, d);
2040
+ }
2029
2041
  this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: agentType || 'main' });
2030
2042
  return {};
2031
2043
  }]
@@ -2395,7 +2407,16 @@ class ClaudeLLMStream extends llm.LLMStream {
2395
2407
  matcher: '.*',
2396
2408
  hooks: [async (input) => {
2397
2409
  console.log('[LIFECYCLE-PROBE] SubagentStart', JSON.stringify(input));
2398
- this.#eventEmitter.emit('agent_started', { agent_type: input?.agent_type, agent_id: input?.agent_id });
2410
+ // Surface WHAT the sub-agent was dispatched to do (not just its type) so the
2411
+ // Running panel shows the task while it's in flight, before any result lands.
2412
+ // The SDK hook may carry the initial prompt/description; fall back to the last
2413
+ // Task/Agent dispatch of this type captured at PreToolUse. Best-effort.
2414
+ const at0 = input?.agent_type;
2415
+ const rawTask = input?.prompt ?? input?.description ?? (at0 ? this.#pendingTaskByType.get(String(at0)) : undefined);
2416
+ if (at0)
2417
+ this.#pendingTaskByType.delete(String(at0));
2418
+ const task = rawTask ? String(rawTask).replace(/\s+/g, ' ').trim().slice(0, 160) : undefined;
2419
+ this.#eventEmitter.emit('agent_started', { agent_type: input?.agent_type, agent_id: input?.agent_id, task });
2399
2420
  // Record the dispatch as LIVE so the frontend can reconcile orphaned
2400
2421
  // "running" rows against statusManager.getActiveDispatchIds(). Without a
2401
2422
  // start-time entry the manager only knew about dispatches at completion,
@@ -12,6 +12,7 @@ import { llm, DEFAULT_API_CONNECT_OPTIONS } from '@livekit/agents';
12
12
  import { ClaudeLLM } from './claude-llm.js';
13
13
  import { askPipelineFastBrain } from './pipeline-fastbrain.js';
14
14
  import { storeExists, openStore, recentRows } from './session-store.js';
15
+ import { statusManager } from './status-manager.js';
15
16
  // ============================================================
16
17
  // SESSION TAIL — compact recent-conversation block prepended to every user turn
17
18
  // so the orchestrator never loses the thread across context compaction.
@@ -185,6 +186,22 @@ export class PipelineDirectLLM extends llm.LLM {
185
186
  // Actual mid-TTS interruption — keep the full [INTERRUPTED] template
186
187
  console.log(`🔇 [pipeline] Enriching: interrupt (spoken=${interruptCtx.spokenText.length} chars, suppressed=${interruptCtx.suppressedText.length} chars)`);
187
188
  this.#claudeLLM.interruptQuery().catch(() => { });
189
+ // An interrupt/cutoff SEVERS in-flight background Task sub-agents — the SDK's
190
+ // interrupt() kills run_in_background Task children and they never fire SubagentStop,
191
+ // so the agent gets NO signal they died and they can't be resumed. statusManager still
192
+ // lists them (start-tracked, never stopped) = the authoritative orphan set. Name them
193
+ // so the agent re-dispatches the necessary ones; mark them terminal so the tracker +
194
+ // Running panel don't leave them stuck "running" forever.
195
+ const orphanedSubagents = statusManager.getActiveDispatches();
196
+ const subagentReminder = orphanedSubagents.length
197
+ ? `SUB-AGENTS SEVERED BY THE CUTOFF: your in-flight background sub-agents were killed by the interrupt and will NOT report back (no completion signal, cannot be resumed): ${orphanedSubagents.map(o => o.subagentType || 'agent').join(', ')}. Check which of these you still need and RE-DISPATCH them fresh — do not wait on them.`
198
+ : `SUB-AGENTS: if you had any background Task sub-agents in flight, an interrupt severs them silently (no completion signal, no resume) — check and re-dispatch any still needed rather than waiting on them.`;
199
+ for (const o of orphanedSubagents) {
200
+ try {
201
+ statusManager.upsertDispatch(o.id, { dispatchState: 'failed' });
202
+ }
203
+ catch { }
204
+ }
188
205
  enrichedMessage = [
189
206
  `[INTERRUPTED] The user interrupted your response mid-speech.`,
190
207
  ``,
@@ -206,6 +223,8 @@ export class PipelineDirectLLM extends llm.LLM {
206
223
  `CONTEXT PRESERVATION (READ THIS):`,
207
224
  `The user has NO memory of unheard content. If any of it bears on their current message — answers a question they just asked, sets up a follow-up they're now asking about, or shows a knowledge gap and fills in a detail they're reacting to — you MUST surface it. Briefly is fine if their message is off-topic or explores a direction they haven't asked about yet. But never assume they remember what they never heard.`,
208
225
  ``,
226
+ subagentReminder,
227
+ ``,
209
228
  `RESPOND with speech first, then act:`,
210
229
  `- ALWAYS reply with at least one spoken sentence before doing any tool calls`,
211
230
  `- If it's a quick side question, answer it then continue where you left off`,
@@ -102,6 +102,16 @@ export declare class StatusManager {
102
102
  * stale row from the dead process is reconciled away on the next poll.
103
103
  */
104
104
  getActiveDispatchIds(): string[];
105
+ /**
106
+ * Non-terminal dispatches with their type — the set considered "in flight". Used on
107
+ * a voice interrupt to name the sub-agents that were just severed: an interrupt kills
108
+ * in-flight Task sub-agents (SDK regression) and they never fire SubagentStop, so they
109
+ * linger here as the authoritative orphan list until we mark them terminal.
110
+ */
111
+ getActiveDispatches(): Array<{
112
+ id: string;
113
+ subagentType?: string;
114
+ }>;
105
115
  /**
106
116
  * Dispatcher v1 — create or update a dispatch entry keyed by tool_use_id.
107
117
  * Creates a minimal TaskStatus shell if the id doesn't yet exist so callers
@@ -201,6 +201,20 @@ export class StatusManager {
201
201
  t.dispatchState !== 'rejected')
202
202
  .map(t => t.id);
203
203
  }
204
+ /**
205
+ * Non-terminal dispatches with their type — the set considered "in flight". Used on
206
+ * a voice interrupt to name the sub-agents that were just severed: an interrupt kills
207
+ * in-flight Task sub-agents (SDK regression) and they never fire SubagentStop, so they
208
+ * linger here as the authoritative orphan list until we mark them terminal.
209
+ */
210
+ getActiveDispatches() {
211
+ return Array.from(this.tasks.values())
212
+ .filter(t => !!t.subagentType &&
213
+ t.dispatchState !== 'completed' &&
214
+ t.dispatchState !== 'failed' &&
215
+ t.dispatchState !== 'rejected')
216
+ .map(t => ({ id: t.id, subagentType: t.subagentType }));
217
+ }
204
218
  /**
205
219
  * Dispatcher v1 — create or update a dispatch entry keyed by tool_use_id.
206
220
  * Creates a minimal TaskStatus shell if the id doesn't yet exist so callers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.245",
3
+ "version": "0.9.247",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {