Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/orchestrator-delegation.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ When the policy is on and `subagent_run` is available:

- The runtime already defaults `subagent_run` to `mode: "background"` under this policy in interactive and RPC sessions, so omit `mode` for ordinary delegation. It returns a task id at once; the terminal stays free and the human keeps typing. Pass a `label` of three to six words naming the work.
- A child `agent_end` retains its latest answer but is not completion: Pi may still retry, compact, or run a queued follow-up. Treat the task as finished only at `agent_settled`; only then release its queue slot, publish its background result, or terminate it. If it exits first, report failure with its retained answer as diagnostics.
- When a background task settles, its result arrives as a message in this session (custom type `gentle-agents.result`, one per task) and starts a new turn if you are idle. Wait for it: end the turn once launches and any non-overlapping work are done. Never poll, sleep, or call `subagent_status`/`subagent_result` for completion.
- When a background task settles, its result arrives as a message in this session (custom type `gentle-agents.result`, one per task) and starts a new turn if you are idle. Wait for it: end the turn once launches and any non-overlapping work are done. Never sleep or periodically poll `subagent_status`/`subagent_result` for completion or cache maintenance. Retain the task ID. Use `subagent_status` only at a real orchestration decision boundary: user-requested inspection, relevant scope change, input request, or suspected abnormal behavior. Never relaunch equivalent work merely because it is queued or running. Cache warming belongs to Pi's native runtime, never to model-driven maintenance turns.
- Do not claim an implementation ready or RDD-ready while its required verification or correction follow-up remains queued. Run the required focused verification before that claim, and retain legitimate post-correction verification. This does not invent a universal full-suite requirement or make a receipt a delivery gate.
- Use `mode: "task"` only when the subagent must ask the human something mid-flight (task-mode dialogs reach the human; background dialogs are dismissed) or when the human asked to wait.
- Launch as many independent tasks as the work has; the runner queues beyond `max_concurrency`. Do not duplicate launches or work, and do not overlap files or topics. Never run parallel writers in one worktree.
Expand Down
10 changes: 10 additions & 0 deletions docs/readme-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,16 @@ One limitation is worth stating. When a pinned profile omits an agent, that agen

Startup installs and refreshes only delegation and review assets. SDD assets are installed/refreshed on demand; status and doctor report never-installed SDD assets as informational, while missing or stale assets from an existing installation identify their owner-specific repair command. User and project overrides are reported separately from package drift. Package refresh preserves overrides; explicit saved model settings may still update existing SDD or custom-agent routing at startup.

### Native cache warming (Pi 0.86.1+)

To allow warming while the parent waits for background results, explicitly set `"cacheWarming": "idle"` in Pi's `settings.json` (user scope: `~/.pi/agent/settings.json`, or project scope: `.pi/settings.json`). Gentle Shell never changes this setting. Native `"streaming"` mode stops when the agent settles: no idle decision is offered for this hook to override. `"off"` remains an opt-out.

Pi owns provider cache-lifetime eligibility, safe replay, scheduling, and the fixed 30-minute idle / one-hour streaming horizons. Unknown provider lifetimes do not get inferred. Real provider requests replace Pi's schedule; Gentle Shell adds no timer or maintenance message. Refresh usage stays outside model context. Warming is best-effort, not a guarantee of a future cache hit.

Ordinary idle decisions retain Pi's 15% continuation assumption. When the active parent owns queued or running background tasks, Gentle Shell treats continuation probability as 1, but still requires estimated cache-miss savings minus refresh cost to be at least $0.05. Restored, foreign-session, foreground, waiting-for-input, and finished tasks do not strengthen that decision. This only overrides candidates Pi actually offers; it never starts, inspects, polls, steers, or duplicates children.

Completion remains push-driven through `gentle-agents.result`. Retain the task ID, end the parent turn when independent work is done, and never sleep or periodically poll status/results to maintain cache or detect completion. Status inspection is for a concrete orchestration decision, not a heartbeat.

### Background subagents policy

Background delegation requires a live interactive/RPC parent and is rejected in `pi -p`, even when the policy is on. Use task mode for bounded print-mode work.
Expand Down
9 changes: 8 additions & 1 deletion extensions/gentle-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
import { createCompletionQueue } from "../lib/agents-completion-delivery.ts";
import { AGENT_MODE, discoverAgents, parseAgentDefinition, loadAgentsConfig, resolveAgentProfile, withPinnedModelProfiles, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
import { installBackgroundCacheWarming } from "../lib/background-cache-warming.ts";
import { isFinished, TASK_EVENT, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
import { AgentRunner, piCommand, abortReasonText, plannedCommands, type RemediationPlan, type RemediationScope, REMEDIATION_PLAN_ENV, parseRemediationPlan, type AskAnswer, type RunnerDeps, type SddChangeSelection, type TaskRequest } from "../lib/agents-runner.ts";
import { ChildMessenger, type IpcEndpoint } from "../lib/agents-messaging.ts";
Expand Down Expand Up @@ -529,6 +530,12 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
// started before /new or /resume stays in the store and comes back with
// its session. Before the first session_start there is nothing to scope by.
const activeSessionId = (): string | undefined => (sessions === undefined ? undefined : sessions.getSessionId() ?? "");
// Pi 0.86.1 adds this event; the package's pinned 0.85.1 types predate it.
installBackgroundCacheWarming(pi as unknown as Parameters<typeof installBackgroundCacheWarming>[0], () => ({
sessionId: activeSessionId(),
ownedTaskIds,
tasks: store.list(activeSessionId()),
}));
const visibleTasks = (): TaskRecord[] => store.list(activeSessionId());
type SessionTransport = { generation: number; sessionId: string; sessionManager: ExtensionContext["sessionManager"]; client: SessionTransportClient; listener: SessionTransportListener; registry: SessionTransportRegistry; close(): Promise<void> };
let transportGeneration = 0;
Expand Down Expand Up @@ -1168,7 +1175,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
if (observe) metricTasks.set(task.id, metrics);
ownedTaskIds.add(task.id);
store.subscribe(task.id, () => { publishActivity(); requestRender(); });
if (request.mode === AGENT_MODE.BACKGROUND) return text(`Started ${task.agent} in the background as task ${task.id}. Use subagent_status or subagent_result with that id.`, taskDetails(task));
if (request.mode === AGENT_MODE.BACKGROUND) return text(`Started ${task.agent} in the background as task ${task.id}. Retain that id; completion is pushed automatically. Never sleep or periodically poll subagent_status/subagent_result for completion or cache maintenance. Inspect status only at a real orchestration decision boundary; never relaunch equivalent queued/running work.`, taskDetails(task));
// A tool call aborted by the host (a human interrupting the turn, a timeout)
// would otherwise leave the child running and end the call with no result and
// no recorded reason. Cancel through the runner so the lifecycle runs and the
Expand Down
38 changes: 38 additions & 0 deletions lib/background-cache-warming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { TaskRecord } from "./agents-protocol.ts";

const ACTION = { WARM: "warm", STOP: "stop" } as const;
type WarmingAction = (typeof ACTION)[keyof typeof ACTION];
interface WarmingDecision {
warmCost: number;
missCost: number;
continuationProbability: number;
action: WarmingAction;
}
interface WarmingOverride { action: WarmingAction }
interface WarmingEvents {
on(event: "cache_warming_decision", handler: (event: WarmingDecision) => WarmingOverride | undefined): unknown;
}
export interface WarmingState {
sessionId: string | undefined;
ownedTaskIds: ReadonlySet<string>;
tasks: Array<Pick<TaskRecord, "id" | "parentSessionId" | "mode" | "status">>;
}

// Pi 0.86.1's native minimum expected savings, in dollars. Ownership raises
// continuation probability to 1, not permission to spend without a benefit.
const MINIMUM_EXPECTED_SAVINGS = 0.05;

export function installBackgroundCacheWarming(pi: WarmingEvents, state: () => WarmingState): void {
pi.on("cache_warming_decision", (event) => {
const { sessionId, ownedTaskIds, tasks } = state();
if (!sessionId || !tasks.some(task => task.parentSessionId === sessionId &&
ownedTaskIds.has(task.id) && task.mode === "background" &&
(task.status === "queued" || task.status === "running"))) return;
// Only native candidates arrive here: Pi owns opt-out, replay safety,
// provider TTLs, fixed horizons, scheduling and usage outside context.
const economic = Number.isFinite(event.warmCost) && event.warmCost >= 0 &&
Number.isFinite(event.missCost) && event.missCost > 0 &&
event.missCost - event.warmCost >= MINIMUM_EXPECTED_SAVINGS;
return { action: economic ? ACTION.WARM : ACTION.STOP };
});
}
Loading
Loading