@pasko70/pibo 2.4.2 → 2.4.3

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.
Files changed (44) hide show
  1. package/dist/agent-runtime/context-build.js +100 -11
  2. package/dist/agent-runtime/profile-validation.js +3 -0
  3. package/dist/agent-runtime/resource-service.js +16 -0
  4. package/dist/agent-runtime/routed-session.js +30 -23
  5. package/dist/agent-runtime/testing/fake-adapter.js +7 -0
  6. package/dist/agent-runtimes/pi/adapter.js +2 -0
  7. package/dist/agent-runtimes/pi/routed-session.js +42 -27
  8. package/dist/agent-runtimes/pi/runtime.js +8 -5
  9. package/dist/apps/chat/web-app.js +21 -6
  10. package/dist/apps/chat-ui/assets/{dist-Cw9po47P.js → dist-C4JGcQjh.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-BeqHbnGN.js → dist-CF92Lv76.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-DTRjeLwO.js → dist-CJ0JS-bE.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-3YG57JXi.js → dist-DrgKyb9n.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-CrDtveZB.js → dist-VoMyV-PE.js} +1 -1
  15. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +1 -0
  16. package/dist/apps/chat-ui/assets/index-DhX1_aRM.js +228 -0
  17. package/dist/apps/chat-ui/index.html +2 -2
  18. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  19. package/dist/apps/vscode-artifacts/{pibo-vscode-ext-2.4.2.vsix → pibo-vscode-ext-2.4.3.vsix} +0 -0
  20. package/dist/cli.js +16 -6
  21. package/dist/core/context-build.js +66 -6
  22. package/dist/core/model-defaults.js +11 -3
  23. package/dist/core/session-router.js +152 -69
  24. package/dist/gateway/server.js +1 -0
  25. package/dist/loops/accounting.js +80 -0
  26. package/dist/loops/service.js +51 -16
  27. package/dist/loops/store.js +50 -14
  28. package/dist/runs/lifecycle.js +26 -1
  29. package/dist/runs/registry.js +29 -47
  30. package/dist/runs/tools.js +23 -22
  31. package/dist/subagents/context.js +48 -0
  32. package/dist/subagents/runtime-selection.js +28 -0
  33. package/dist/subagents/tool.js +28 -6
  34. package/dist/tools/codex-compat.js +1 -0
  35. package/dist/tools/contract.js +10 -0
  36. package/dist/tools/mcp-bridge.js +4 -2
  37. package/dist/tools/runtime/node-backend.js +8 -2
  38. package/dist/tools/runtime/python-backend.js +8 -2
  39. package/dist/tools/runtime/tool.js +1 -0
  40. package/dist/tools/session-tool-set.js +19 -12
  41. package/npm-shrinkwrap.json +2 -2
  42. package/package.json +1 -1
  43. package/dist/apps/chat-ui/assets/index-AjnP3ci-.js +0 -228
  44. package/dist/apps/chat-ui/assets/index-BJ56TREg.css +0 -1
@@ -4,7 +4,7 @@ import { dirname, resolve } from 'node:path';
4
4
  import { DatabaseSync } from 'node:sqlite';
5
5
  import { piboHomePath } from '../core/pibo-home.js';
6
6
  import { isPiboThinkingLevel } from '../core/thinking.js';
7
- import { newGoalTokenAccounting, normalizeLoopTokenAccounting } from './accounting.js';
7
+ import { addLoopAssistantUsage, newGoalTokenAccounting, normalizeLoopTokenAccounting } from './accounting.js';
8
8
  function nowIso(now = new Date()) { return now.toISOString(); }
9
9
  function parseJson(json) { return JSON.parse(json); }
10
10
  function defaultName(prompt) { const normalized = prompt.replace(/\s+/g, ' ').trim(); return normalized ? normalized.slice(0, 80) : 'Loop job'; }
@@ -418,29 +418,65 @@ export class PiboLoopStore {
418
418
  this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(budgetLimited ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, id);
419
419
  return this.getJob(id);
420
420
  }
421
- recordGoalTurnUsage(id, runId, tokens, now = new Date()) {
421
+ recordGoalAssistantUsage(id, runId, input, now = new Date()) {
422
422
  this.db.exec('BEGIN IMMEDIATE');
423
423
  try {
424
- const job = this.recordGoalProgress(id, { tokens }, now);
425
- if (job?.mode === 'goal') {
426
- const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, id);
427
- if (row) {
428
- const accounting = parseRunAccounting(row.accounting_json) ?? { tokenAccounting: normalizeLoopTokenAccounting(job.state.tokenAccounting) };
429
- const turnTokens = (accounting.tokensUsed ?? 0) + Math.max(0, Math.floor(tokens));
430
- const budget = accounting.tokenBudget;
431
- const before = accounting.tokensUsedBefore ?? 0;
432
- const nextAccounting = { ...accounting, tokensUsed: turnTokens, ...(budget !== undefined ? { overshootTokens: Math.max(0, before + turnTokens - budget) } : {}) };
433
- this.db.prepare('UPDATE pibo_ralph_runs SET accounting_json = ?, updated_at = ? WHERE id = ?').run(runAccountingJson(nextAccounting), nowIso(now), runId);
434
- }
424
+ const job = this.getJob(id);
425
+ if (!job || job.mode !== 'goal') {
426
+ this.db.exec('COMMIT');
427
+ return job;
428
+ }
429
+ const tokens = Math.max(0, Math.floor(input.budgetTokens));
430
+ const nextTokens = (job.state.tokensUsed ?? 0) + tokens;
431
+ const currentStatus = goalStatus(job) ?? 'active';
432
+ const budgetLimited = currentStatus === 'active' && job.tokenBudget !== undefined && nextTokens >= job.tokenBudget;
433
+ const timestamp = nowIso(now);
434
+ const state = {
435
+ ...job.state,
436
+ tokensUsed: nextTokens,
437
+ usage: addLoopAssistantUsage(job.state.usage, input.usage, {
438
+ piboSessionId: input.piboSessionId,
439
+ descendant: input.descendant,
440
+ }),
441
+ goalStatus: budgetLimited ? 'budget_limited' : currentStatus,
442
+ };
443
+ if (budgetLimited)
444
+ state.goalEndedAt = job.state.goalEndedAt ?? timestamp;
445
+ this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(budgetLimited ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, id);
446
+ const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, id);
447
+ if (row) {
448
+ const accounting = parseRunAccounting(row.accounting_json) ?? { tokenAccounting: normalizeLoopTokenAccounting(state.tokenAccounting) };
449
+ const turnTokens = (accounting.tokensUsed ?? 0) + tokens;
450
+ const budget = accounting.tokenBudget;
451
+ const before = accounting.tokensUsedBefore ?? 0;
452
+ const nextAccounting = {
453
+ ...accounting,
454
+ tokensUsed: turnTokens,
455
+ usage: addLoopAssistantUsage(accounting.usage, input.usage, {
456
+ piboSessionId: input.piboSessionId,
457
+ descendant: input.descendant,
458
+ }),
459
+ ...(budget !== undefined ? { overshootTokens: Math.max(0, before + turnTokens - budget) } : {}),
460
+ };
461
+ this.db.prepare('UPDATE pibo_ralph_runs SET accounting_json = ?, updated_at = ? WHERE id = ?').run(runAccountingJson(nextAccounting), timestamp, runId);
435
462
  }
436
463
  this.db.exec('COMMIT');
437
- return job;
464
+ return this.getJob(id);
438
465
  }
439
466
  catch (error) {
440
467
  this.db.exec('ROLLBACK');
441
468
  throw error;
442
469
  }
443
470
  }
471
+ recordGoalTurnUsage(id, runId, tokens, now = new Date()) {
472
+ const run = this.getRun(runId);
473
+ return this.recordGoalAssistantUsage(id, runId, {
474
+ usage: { type: 'assistant_usage', piboSessionId: run?.piboSessionId ?? 'unknown', totalTokens: Math.max(0, Math.floor(tokens)) },
475
+ budgetTokens: tokens,
476
+ piboSessionId: run?.piboSessionId ?? 'unknown',
477
+ descendant: false,
478
+ }, now);
479
+ }
444
480
  recordGoalRunTime(id, runId, activeTimeSeconds, now = new Date()) {
445
481
  const seconds = Math.max(0, Math.floor(activeTimeSeconds));
446
482
  this.db.exec('BEGIN IMMEDIATE');
@@ -1,3 +1,4 @@
1
+ export const PIBO_RUN_CANCELLATION_SETTLEMENT_TIMEOUT_MS = 15_000;
1
2
  export class PiboRunExecutionTimeoutError extends Error {
2
3
  timeoutPhase;
3
4
  constructor(message, timeoutPhase) {
@@ -18,6 +19,22 @@ export class PiboRunCancelledError extends Error {
18
19
  this.name = "PiboRunCancelledError";
19
20
  }
20
21
  }
22
+ export async function waitForRunCancellationSettlement(settled, timeoutMs = PIBO_RUN_CANCELLATION_SETTLEMENT_TIMEOUT_MS) {
23
+ let timer;
24
+ try {
25
+ await Promise.race([
26
+ settled,
27
+ new Promise((_resolve, reject) => {
28
+ timer = setTimeout(() => reject(new Error(`Yielded run did not settle within ${timeoutMs}ms after cancellation.`)), timeoutMs);
29
+ timer.unref?.();
30
+ }),
31
+ ]);
32
+ }
33
+ finally {
34
+ if (timer)
35
+ clearTimeout(timer);
36
+ }
37
+ }
21
38
  export function resolveRunTimeoutMs(toolName, params) {
22
39
  if (!params || typeof params !== "object" || Array.isArray(params))
23
40
  return undefined;
@@ -38,7 +55,15 @@ export function foregroundServiceWarning(toolName, params, timeoutMs) {
38
55
  }
39
56
  export function isConfiguredTimeoutError(error) {
40
57
  const message = error instanceof Error ? error.message : String(error);
41
- return /(?:timed?\s*out|timeout)/i.test(message);
58
+ const terminalLines = message.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-5);
59
+ return terminalLines.some((line) => {
60
+ const normalized = line.replace(/^error:\s*/i, "");
61
+ return /^(?:command|process|tool execution|yielded run)\s+timed?\s*out\b.*$/i.test(normalized)
62
+ || /^timed?\s*out(?:\s+after\s+.+)?[.!]?$/i.test(normalized)
63
+ || /^timeout(?:\s+error)?[.!]?$/i.test(normalized)
64
+ || /^timeout(?::|\s+)(?:occurred|expired|exceeded|elapsed|reached)\b.*$/i.test(normalized)
65
+ || /^timeout(?::|\s+)(?:after\s+)?\d+(?:\.\d+)?\s*(?:ms|milliseconds?|s|secs?|seconds?|m|mins?|minutes?|h|hours?)\b.*$/i.test(normalized);
66
+ });
42
67
  }
43
68
  export function hasMeaningfulTimeoutOutput(value) {
44
69
  const text = extractText(value);
@@ -10,12 +10,21 @@ function runTimeoutAt(createdAt, timeoutMs) {
10
10
  function sameOrigin(left, right) {
11
11
  if (!left || !right)
12
12
  return left === right;
13
- return left.eventId === right.eventId
14
- && left.provenance.kind === right.provenance.kind
15
- && left.provenance.jobId === right.provenance.jobId
16
- && left.provenance.runId === right.provenance.runId
17
- && left.provenance.cause === right.provenance.cause
18
- && left.provenance.rootEventId === right.provenance.rootEventId;
13
+ if (left.eventId !== right.eventId || left.provenance.kind !== right.provenance.kind)
14
+ return false;
15
+ if (left.provenance.kind === "loop-run" && right.provenance.kind === "loop-run") {
16
+ return left.provenance.jobId === right.provenance.jobId
17
+ && left.provenance.runId === right.provenance.runId
18
+ && left.provenance.cause === right.provenance.cause
19
+ && left.provenance.rootEventId === right.provenance.rootEventId;
20
+ }
21
+ if (left.provenance.kind === "subagent-request" && right.provenance.kind === "subagent-request") {
22
+ return left.provenance.requestId === right.provenance.requestId
23
+ && left.provenance.controllerPiboSessionId === right.provenance.controllerPiboSessionId
24
+ && left.provenance.loopJobId === right.provenance.loopJobId
25
+ && left.provenance.loopRunId === right.provenance.loopRunId;
26
+ }
27
+ return false;
19
28
  }
20
29
  function formatTimeout(timeoutMs) {
21
30
  if (timeoutMs === undefined)
@@ -218,6 +227,16 @@ export class PiboRunRegistry {
218
227
  .filter((record) => options.includeDetached || record.completionPolicy !== "detached")
219
228
  .map(snapshot);
220
229
  }
230
+ listActiveControllerRuns(controllerPiboSessionId) {
231
+ return [...this.runs.values()]
232
+ .filter((record) => record.controllerPiboSessionId === controllerPiboSessionId && !terminal(record.status))
233
+ .map(snapshot);
234
+ }
235
+ listActiveRuns() {
236
+ return [...this.runs.values()]
237
+ .filter((record) => !terminal(record.status))
238
+ .map(snapshot);
239
+ }
221
240
  status(controllerPiboSessionId, runId) {
222
241
  return snapshot(this.requireRunForController(controllerPiboSessionId, runId));
223
242
  }
@@ -270,21 +289,22 @@ export class PiboRunRegistry {
270
289
  output.error = record.error;
271
290
  return output;
272
291
  }
273
- cancel(controllerPiboSessionId, runId) {
292
+ cancel(controllerPiboSessionId, runId, reason = "Run was cancelled.") {
274
293
  const record = this.requireRunForController(controllerPiboSessionId, runId);
275
294
  const previousStatus = record.status;
276
295
  if (!terminal(record.status)) {
277
296
  record.status = "cancelled";
297
+ record.error = reason;
278
298
  record.summary = `${record.toolName} run cancelled.`;
279
299
  this.finish(record);
280
300
  if (record.jobId)
281
- this.options.store?.fail(record.jobId, this.workerId, "Run was cancelled.");
301
+ this.options.store?.fail(record.jobId, this.workerId, reason);
282
302
  }
283
303
  record.consumed = true;
284
304
  record.updatedAt = now();
285
305
  this.options.store?.updateRun(runId, record);
286
306
  const output = snapshot(record);
287
- this.notify({ type: "run_changed", run: output, previousStatus, reason: "Run was cancelled." });
307
+ this.notify({ type: "run_changed", run: output, previousStatus, reason });
288
308
  return output;
289
309
  }
290
310
  ack(controllerPiboSessionId, runId) {
@@ -356,44 +376,6 @@ export class PiboRunRegistry {
356
376
  hasPendingNotification(controllerPiboSessionId, options = {}) {
357
377
  return [...this.runs.values()].some((record) => this.needsNotification(record, controllerPiboSessionId, options));
358
378
  }
359
- cancelControllerRuns(controllerPiboSessionId, reason = "Controller Pibo session was disposed.") {
360
- const cancelled = [];
361
- for (const record of this.runs.values()) {
362
- if (record.controllerPiboSessionId !== controllerPiboSessionId || terminal(record.status))
363
- continue;
364
- record.status = "cancelled";
365
- record.error = reason;
366
- record.consumed = true;
367
- record.summary = `${record.toolName} run cancelled.`;
368
- this.finish(record);
369
- this.options.store?.updateRun(record.runId, record);
370
- if (record.jobId)
371
- this.options.store?.fail(record.jobId, this.workerId, reason);
372
- const output = snapshot(record);
373
- this.notify({ type: "run_changed", run: output, previousStatus: "running", reason });
374
- cancelled.push(output);
375
- }
376
- return cancelled;
377
- }
378
- cancelAll(reason = "Run registry was disposed.") {
379
- const cancelled = [];
380
- for (const record of this.runs.values()) {
381
- if (terminal(record.status))
382
- continue;
383
- record.status = "cancelled";
384
- record.error = reason;
385
- record.consumed = true;
386
- record.summary = `${record.toolName} run cancelled.`;
387
- this.finish(record);
388
- this.options.store?.updateRun(record.runId, record);
389
- if (record.jobId)
390
- this.options.store?.fail(record.jobId, this.workerId, reason);
391
- const output = snapshot(record);
392
- this.notify({ type: "run_changed", run: output, previousStatus: "running", reason });
393
- cancelled.push(output);
394
- }
395
- return cancelled;
396
- }
397
379
  prune(options = {}) {
398
380
  const nowMs = options.nowMs ?? Date.now();
399
381
  const consumedTerminalTtlMs = options.consumedTerminalTtlMs ??
@@ -1,8 +1,17 @@
1
1
  import { Type } from "typebox";
2
2
  import { piboStringEnum } from "../tools/schema.js";
3
- import { definePiboTool } from "../tools/contract.js";
4
- import { foregroundServiceWarning, hasMeaningfulTimeoutOutput, isConfiguredTimeoutError, PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError, resolveRunTimeoutMs } from "./lifecycle.js";
3
+ import { definePiboTool, piboToolTerminalStatus, piboToolTimeoutPhase } from "../tools/contract.js";
4
+ import { foregroundServiceWarning, hasMeaningfulTimeoutOutput, isConfiguredTimeoutError, PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError, resolveRunTimeoutMs, waitForRunCancellationSettlement } from "./lifecycle.js";
5
5
  import { PiboRunResourceLimitError, prepareYieldedRunExecution } from "./resource-isolation.js";
6
+ export const PIBO_RUN_TOOL_NAMES = [
7
+ "pibo_run_start",
8
+ "pibo_run_list",
9
+ "pibo_run_status",
10
+ "pibo_run_wait",
11
+ "pibo_run_read",
12
+ "pibo_run_cancel",
13
+ "pibo_run_ack",
14
+ ];
6
15
  function resultText(prefix, value) {
7
16
  return `${prefix}\n${JSON.stringify(value, null, 2)}`;
8
17
  }
@@ -27,22 +36,6 @@ function requireTool(tools, name) {
27
36
  }
28
37
  return tool;
29
38
  }
30
- async function waitForRunCancellationSettlement(settled, timeoutMs = 15_000) {
31
- let timer;
32
- try {
33
- await Promise.race([
34
- settled,
35
- new Promise((_resolve, reject) => {
36
- timer = setTimeout(() => reject(new Error(`Yielded run did not settle within ${timeoutMs}ms after cancellation.`)), timeoutMs);
37
- timer.unref?.();
38
- }),
39
- ]);
40
- }
41
- finally {
42
- if (timer)
43
- clearTimeout(timer);
44
- }
45
- }
46
39
  export function createRunToolDefinitions(yieldableTools, controller) {
47
40
  const toolNames = yieldableTools.map((tool) => tool.name);
48
41
  return [
@@ -99,18 +92,23 @@ export function createRunToolDefinitions(yieldableTools, controller) {
99
92
  if (cancellationFailure)
100
93
  throw cancellationFailure;
101
94
  },
102
- async execute() {
95
+ async execute(runId) {
103
96
  executionStarted = true;
104
97
  try {
105
98
  const result = await prepared.execute(() => tool.execute(toolCallId, prepared.params, runSignal, (update) => {
106
99
  observedOutput ||= hasMeaningfulTimeoutOutput(update);
107
100
  onUpdate?.(update);
108
- }, ctx));
101
+ }, { ...ctx, yieldedRunId: runId }));
109
102
  const resultObject = result;
110
103
  const text = textFromToolResult(resultObject);
111
104
  if (resultObject.isError === true) {
112
- if (timeoutMs !== undefined && isConfiguredTimeoutError(text ?? ""))
105
+ const structuredTimeout = piboToolTerminalStatus(resultObject) === "timed_out";
106
+ if (structuredTimeout) {
107
+ throw new PiboRunExecutionTimeoutError(text ?? `${tool.name} timed out.`, piboToolTimeoutPhase(resultObject) ?? (observedOutput || hasMeaningfulTimeoutOutput(text) ? "lifetime" : "startup"));
108
+ }
109
+ if (timeoutMs !== undefined && isConfiguredTimeoutError(text ?? "")) {
113
110
  throw new PiboRunExecutionTimeoutError(text ?? `${tool.name} timed out.`, observedOutput || hasMeaningfulTimeoutOutput(text) ? "lifetime" : "startup");
111
+ }
114
112
  throw new Error(text ?? `${tool.name} returned an error result.`);
115
113
  }
116
114
  return { text, details: resultObject.details ?? result };
@@ -231,8 +229,11 @@ export function createRunToolDefinitions(yieldableTools, controller) {
231
229
  }),
232
230
  async execute(_toolCallId, params) {
233
231
  const run = await controller.cancelRun(params.runId);
232
+ const prefix = run.status === "cancelled"
233
+ ? `Cancelled run ${run.runId}.`
234
+ : `Run ${run.runId} reached ${run.status} before cancellation completed.`;
234
235
  return {
235
- content: [{ type: "text", text: resultText(`Cancelled run ${run.runId}.`, run) }],
236
+ content: [{ type: "text", text: resultText(prefix, run) }],
236
237
  details: run,
237
238
  };
238
239
  },
@@ -0,0 +1,48 @@
1
+ import { listAvailableAgents } from "./tool.js";
2
+ export const PIBO_DELEGATED_AGENT_CONTEXT_PATH = "pibo://runtime/delegated-agents.md";
3
+ export function getDelegatedAgentContextFile(subagents) {
4
+ const agents = listAvailableAgents(subagents);
5
+ if (agents.length === 0)
6
+ return undefined;
7
+ const catalog = agents.map((agent) => {
8
+ const runtime = [
9
+ agent.model ? `${agent.model.provider}/${agent.model.id}` : undefined,
10
+ agent.thinkingLevel ? `thinking ${agent.thinkingLevel}` : undefined,
11
+ ].filter(Boolean).join(", ");
12
+ return `- \`${agent.name}\` → \`${agent.profile}\`${runtime ? ` (${runtime})` : ""}: ${agent.description}`;
13
+ }).join("\n");
14
+ return {
15
+ path: PIBO_DELEGATED_AGENT_CONTEXT_PATH,
16
+ content: [
17
+ "# Delegated Agent Management",
18
+ "",
19
+ "This session has Pibo-managed delegated agents. Dispatch is yielded-only: never call `pibo_agents_send_message` directly. Start it through `pibo_run_start`, then manage the returned run ID.",
20
+ "",
21
+ "## Available agents",
22
+ "",
23
+ catalog,
24
+ "",
25
+ "## Required workflow",
26
+ "",
27
+ "```text",
28
+ "pibo_run_start({",
29
+ " toolName: \"pibo_agents_send_message\",",
30
+ " arguments: { name, message, threadKey? },",
31
+ " completionPolicy?: \"tracked\" | \"detached\"",
32
+ "}) -> { runId }",
33
+ "",
34
+ "pibo_run_wait({ runId, timeoutMs? }) # bounded wait only; expiry does not stop the child",
35
+ "pibo_run_status({ runId }) # compact lifecycle state",
36
+ "pibo_agents_observe({ requestIds?: [runId], agentIds?, names?, threadKeys?, kinds?, roles?, ... })",
37
+ "pibo_run_read({ runId }) # terminal result, including the complete final agent message",
38
+ "pibo_run_cancel({ runId }) # explicit request cancellation",
39
+ "pibo_agents_list_agents({}) # available definitions and persistent child instances",
40
+ "pibo_agents_kill({ agentId }) # terminate one persistent child session subtree",
41
+ "```",
42
+ "",
43
+ "Reuse a stable `threadKey` to continue the same child Pibo Session. A wait timeout is only an orchestrator wake-up. Observe progress and decide whether to continue waiting, steer through a new message after the current turn, cancel the request, or kill the child session.",
44
+ "",
45
+ "For substantial reports, ask the child to persist a Markdown artifact and include its path in the complete final message.",
46
+ ].join("\n"),
47
+ };
48
+ }
@@ -0,0 +1,28 @@
1
+ import { selectRequestedSubagentModelProfile, selectRequestedSubagentThinkingLevel, } from "../core/model-defaults.js";
2
+ export function resolvePiboSubagentRuntimeSelection(subagent, targetProfile, modelDefaults = {}) {
3
+ const effectiveModel = subagent.model ?? selectRequestedSubagentModelProfile(targetProfile, modelDefaults);
4
+ const effectiveThinkingLevel = subagent.thinkingLevel ?? selectRequestedSubagentThinkingLevel(targetProfile, modelDefaults);
5
+ return {
6
+ ...(subagent.model ? { configuredModel: { ...subagent.model } } : {}),
7
+ ...(effectiveModel ? { effectiveModel: { ...effectiveModel } } : {}),
8
+ ...(subagent.thinkingLevel ? { configuredThinkingLevel: subagent.thinkingLevel } : {}),
9
+ ...(effectiveThinkingLevel ? { effectiveThinkingLevel } : {}),
10
+ };
11
+ }
12
+ export function resolvePiboSubagentRuntimeSelections(subagents, targetProfileResolver, modelDefaults = {}) {
13
+ return subagents.map((subagent) => {
14
+ const targetProfile = targetProfileResolver?.(subagent.targetProfile);
15
+ const selection = targetProfile
16
+ ? resolvePiboSubagentRuntimeSelection(subagent, targetProfile, modelDefaults)
17
+ : {
18
+ ...(subagent.model ? { configuredModel: { ...subagent.model }, effectiveModel: { ...subagent.model } } : {}),
19
+ ...(subagent.thinkingLevel ? { configuredThinkingLevel: subagent.thinkingLevel, effectiveThinkingLevel: subagent.thinkingLevel } : {}),
20
+ };
21
+ return {
22
+ name: subagent.name,
23
+ targetProfile: subagent.targetProfile,
24
+ enabled: subagent.enabled !== false,
25
+ ...selection,
26
+ };
27
+ });
28
+ }
@@ -30,6 +30,13 @@ export function formatAvailableAgentsForPrompt(subagents) {
30
30
  function resultText(prefix, value) {
31
31
  return `${prefix}\n${JSON.stringify(value, null, 2)}`;
32
32
  }
33
+ function normalizeAgentSendMessageResult(result, fallbackRequestId) {
34
+ return {
35
+ ...result,
36
+ requestId: result.requestId?.trim() || fallbackRequestId,
37
+ finalMessage: typeof result.finalMessage === "string" ? result.finalMessage : result.reply.text,
38
+ };
39
+ }
33
40
  export function createAgentToolDefinitions(subagents, controller) {
34
41
  const enabled = subagents.filter((subagent) => subagent.enabled !== false);
35
42
  if (enabled.length === 0)
@@ -48,11 +55,11 @@ export function createAgentToolDefinitions(subagents, controller) {
48
55
  name: "pibo_agents_send_message",
49
56
  title: "Pibo Agents Send Message",
50
57
  description: [
51
- "Send a message to an available delegated agent. Foreground execution waits for the reply; use pibo_run_start for asynchronous delegation.",
58
+ "Yielded-only delegated send. Start this tool through pibo_run_start; bounded waits do not limit the child lifetime.",
52
59
  "Available agents:",
53
60
  catalog,
54
61
  ].join("\n"),
55
- promptSnippet: "Send work to an available delegated agent by name. Reuse threadKey to continue its child session. Use pibo_run_start with this tool for asynchronous work. The tool definition lists the available names and parent-visible descriptions.",
62
+ promptSnippet: "Start pibo_agents_send_message through pibo_run_start. Reuse threadKey to continue its child session, and use run wait/status/read/cancel plus agent observe for lifecycle control.",
56
63
  executionMode: "parallel",
57
64
  inputSchema: Type.Object({
58
65
  name: piboStringEnum(names, { description: "Available delegated agent name" }),
@@ -62,22 +69,35 @@ export function createAgentToolDefinitions(subagents, controller) {
62
69
  maxLength: 256,
63
70
  })),
64
71
  }),
65
- async execute(toolCallId, params, signal) {
72
+ async execute(toolCallId, params, signal, _onUpdate, context) {
66
73
  const subagent = byName.get(params.name);
67
74
  if (!subagent)
68
75
  throw new Error(`Unknown delegated agent "${params.name}"`);
69
- const result = await controller.sendMessage({
76
+ if (!context.yieldedRunId) {
77
+ throw new Error("pibo_agents_send_message is yielded-only. Start it through pibo_run_start.");
78
+ }
79
+ const result = normalizeAgentSendMessageResult(await controller.sendMessage({
70
80
  subagent,
71
81
  message: params.message,
72
82
  threadKey: params.threadKey,
73
83
  toolCallId,
84
+ requestId: context.yieldedRunId,
85
+ parentProvenance: context.getActiveMessage?.()?.provenance,
74
86
  signal,
75
- });
87
+ }), context.yieldedRunId);
76
88
  return {
77
89
  content: [{
78
90
  type: "text",
79
- text: `Agent ${result.name} (${result.agentId}, thread ${result.threadKey}) replied:\n${result.reply.text}`,
91
+ text: `Agent request ${result.requestId} completed (${result.name}, ${result.agentId}, thread ${result.threadKey}).\n\n${result.finalMessage}`,
80
92
  }],
93
+ structuredContent: {
94
+ status: "completed",
95
+ requestId: result.requestId,
96
+ agentId: result.agentId,
97
+ threadKey: result.threadKey,
98
+ eventId: result.eventId,
99
+ finalMessage: result.finalMessage,
100
+ },
81
101
  details: result,
82
102
  };
83
103
  },
@@ -106,11 +126,13 @@ export function createAgentToolDefinitions(subagents, controller) {
106
126
  executionMode: "parallel",
107
127
  annotations: { readOnly: true },
108
128
  inputSchema: Type.Object({
129
+ requestIds: Type.Optional(Type.Array(Type.String({ description: "Exact yielded run/request ID" }), { maxItems: 50 })),
109
130
  agentIds: Type.Optional(Type.Array(Type.String({ description: "Owned child agentId" }), { maxItems: 50 })),
110
131
  names: Type.Optional(Type.Array(piboStringEnum(names), { maxItems: 50 })),
111
132
  threadKeys: Type.Optional(Type.Array(Type.String(), { maxItems: 50 })),
112
133
  eventTypes: Type.Optional(Type.Array(Type.String({ description: "Exact Pibo output event type" }), { maxItems: 50 })),
113
134
  kinds: Type.Optional(Type.Array(piboStringEnum(["message", "thinking", "tool", "error", "lifecycle", "event"]), { maxItems: 6 })),
135
+ roles: Type.Optional(Type.Array(Type.String({ description: "Exact normalized role, for example assistant" }), { maxItems: 20 })),
114
136
  since: Type.Optional(Type.String({ description: "Inclusive ISO-8601 lower timestamp bound" })),
115
137
  until: Type.Optional(Type.String({ description: "Inclusive ISO-8601 upper timestamp bound" })),
116
138
  textContains: Type.Optional(Type.String({ description: "Case-insensitive substring match against normalized observation text" })),
@@ -4,6 +4,7 @@ import { extname, isAbsolute, resolve } from "node:path";
4
4
  import { Type } from "typebox";
5
5
  import { piboStringEnum } from "./schema.js";
6
6
  import { definePiboTool } from "./contract.js";
7
+ export const CODEX_COMPAT_TOOL_NAMES = ["apply_patch", "view_image"];
7
8
  function resolveCwd(baseCwd, workdir) {
8
9
  if (!workdir || workdir.trim().length === 0)
9
10
  return baseCwd;
@@ -1,3 +1,10 @@
1
+ export function piboToolTerminalStatus(result) {
2
+ return result.metadata?.piboTerminalStatus === "timed_out" ? "timed_out" : undefined;
3
+ }
4
+ export function piboToolTimeoutPhase(result) {
5
+ const phase = result.metadata?.piboTimeoutPhase;
6
+ return phase === "startup" || phase === "lifetime" ? phase : undefined;
7
+ }
1
8
  /** Identity helper that preserves schema-derived input types. */
2
9
  export function definePiboTool(definition) {
3
10
  definition.label ??= definition.title;
@@ -14,8 +21,11 @@ export function isPiboToolDefinition(value) {
14
21
  export function normalizePiboToolResult(result) {
15
22
  return {
16
23
  content: result.content.map((content) => ({ ...content })),
24
+ ...("structuredContent" in result && result.structuredContent !== undefined ? { structuredContent: result.structuredContent } : {}),
17
25
  ...(result.details !== undefined ? { details: result.details } : {}),
18
26
  ...(result.isError !== undefined ? { isError: result.isError } : {}),
27
+ ...("payloadRefs" in result && result.payloadRefs !== undefined ? { payloadRefs: [...result.payloadRefs] } : {}),
28
+ ...("metadata" in result && result.metadata !== undefined ? { metadata: { ...result.metadata } } : {}),
19
29
  };
20
30
  }
21
31
  /** Convert a legacy Pi-shaped definition without leaking Pi types into generic code. */
@@ -166,10 +166,12 @@ async function convertContent(items, options) {
166
166
  return { content, payloadRefs };
167
167
  }
168
168
  async function piboResultToMcp(result, options) {
169
- const converted = await convertContent(result.content, options);
169
+ const preserveCompleteRunRead = options.tool.name === "pibo_run_read";
170
+ const conversionOptions = preserveCompleteRunRead ? { ...options, writer: undefined } : options;
171
+ const converted = await convertContent(result.content, conversionOptions);
170
172
  const payloadRefs = [...new Set([...(result.payloadRefs ?? []), ...converted.payloadRefs])];
171
173
  let structuredContent = result.structuredContent ?? toJsonValue(result.details);
172
- if (structuredContent !== undefined && options.writer) {
174
+ if (structuredContent !== undefined && options.writer && !preserveCompleteRunRead) {
173
175
  const encoded = JSON.stringify(structuredContent);
174
176
  if (Buffer.byteLength(encoded, "utf8") > options.threshold) {
175
177
  const stored = await storeLargeContent({
@@ -68,8 +68,14 @@ export class NodeRuntimeBackend {
68
68
  static async start(baseCwd, input) {
69
69
  const target = input.target ?? {};
70
70
  const backend = new NodeRuntimeBackend(resolveCwd(baseCwd, target.cwd), target.executable ?? process.execPath, target.args ?? [], target.env);
71
- await backend.waitReady(input.timeoutMs ?? 10000);
72
- return backend;
71
+ try {
72
+ await backend.waitReady(input.timeoutMs ?? 10000);
73
+ return backend;
74
+ }
75
+ catch (error) {
76
+ await backend.close(true).catch(() => undefined);
77
+ throw error;
78
+ }
73
79
  }
74
80
  isAlive() {
75
81
  return this.alive && !this.child.killed;
@@ -68,8 +68,14 @@ export class PythonRuntimeBackend {
68
68
  static async start(baseCwd, input) {
69
69
  const target = input.target ?? {};
70
70
  const backend = new PythonRuntimeBackend(resolveCwd(baseCwd, target.cwd), target.executable ?? (process.platform === "win32" ? "python" : "python3"), target.args ?? [], target.env);
71
- await backend.waitReady(input.timeoutMs ?? 10000);
72
- return backend;
71
+ try {
72
+ await backend.waitReady(input.timeoutMs ?? 10000);
73
+ return backend;
74
+ }
75
+ catch (error) {
76
+ await backend.close(true).catch(() => undefined);
77
+ throw error;
78
+ }
73
79
  }
74
80
  isAlive() {
75
81
  return this.alive && !this.child.killed;
@@ -163,6 +163,7 @@ export function createRuntimeToolDefinition(controller) {
163
163
  content: [{ type: "text", text: formatRuntimeResult(result) }],
164
164
  details: result,
165
165
  isError: isErrorStatus(status),
166
+ ...(status === "timeout" ? { metadata: { piboTerminalStatus: "timed_out", piboTimeoutPhase: "startup" } } : {}),
166
167
  };
167
168
  },
168
169
  });