Initialize repository with harness code and assets
Initial import of all source code, config, and README assets: the packages workspace (cli, core, server, web, docs, landing, skills), build scripts, tooling config, and CI workflows. Includes the data-layout revision made on this branch: the local data root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the installer keeps its binaries in ~/.penguin), and every Agent lives under <project>/agents/<agent>/ — path helpers, the three agent-enumeration scans, the system prompt, built-in Skills, tests and docs all follow the new layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* run_subagent — delegates a subtask to a child Agent, supporting a switch to long-running
|
||||
* background execution.
|
||||
*
|
||||
* The tool itself doesn't depend on Agent/Session, only holding an injected `SubagentRunner`
|
||||
* (breaking the circular dependency). The model may freely choose the child Agent (`agent_id`)
|
||||
* and model (`model_id`) via arguments; if omitted, it falls back to reusing the current Agent
|
||||
* and the Project's default Model respectively. The spawned child session is managed by
|
||||
* `ManagedSubagentSession` (sharing the `SubagentSessionManager` injected by Environment with
|
||||
* `input_subagent`).
|
||||
*
|
||||
* The two-phase semantics mirror `exec_command`: within the `yield_time_ms` window, child-session
|
||||
* messages are forwarded live (tagged with origin, so the frontend can see the child Agent's tool
|
||||
* calls and token usage) and the child Agent's text deltas are copied as this tool's output; if
|
||||
* the child Agent finishes within the window, its terminal state is returned and the child
|
||||
* session is released; if it's still running once the window expires, it's registered as a
|
||||
* background session, returning `subagent_id` for subsequent access the same way as
|
||||
* `input_command` (polling / appending a Prompt, see input-subagent.ts).
|
||||
*
|
||||
* Approval: `run_subagent` itself is a read-write tool (`rw`), so its invocation requires Human
|
||||
* approval; the child session's tool approval requests are forwarded to the same Human within
|
||||
* the window via the session's approval queue (tagged with origin), and queued for the next
|
||||
* access while running in the background. An interruption within the startup window kills the
|
||||
* child session per exec_command semantics; precheck errors such as exceeding the depth limit or
|
||||
* a nonexistent agent are expressed by the runner as a throw, and collapsed to failed.
|
||||
* Docs: /docs/tools § "Subagents".
|
||||
*/
|
||||
import { partialToolCallOutput } from "../../omnimessage/index.js";
|
||||
import type { OmniMessage } from "../../omnimessage/index.js";
|
||||
import type { EnvironmentServices, ToolDefinitionConfig } from "../../interfaces.js";
|
||||
import type { BuiltinTool, ToolExecutionContext, ToolResult } from "./types.js";
|
||||
import {
|
||||
DEFAULT_SUBAGENT_YIELD_MS,
|
||||
ManagedSubagentSession,
|
||||
resultForSubagentExit,
|
||||
} from "./subagent/index.js";
|
||||
import { collectWindow } from "./subagent/collect.js";
|
||||
import { clampYield } from "./background/index.js";
|
||||
|
||||
/** Tool name constant (used only within this tool module, never exposed to Environment). */
|
||||
export const SUBAGENT_NAME = "run_subagent";
|
||||
|
||||
/** Pending-approval hint: lets the model know it should poll again to move the child Agent forward. */
|
||||
export function approvalHint(session: ManagedSubagentSession): string {
|
||||
const n = session.pendingApprovals;
|
||||
return n > 0 ? ` [subagent is waiting for approval of ${n} tool call(s); poll to review]` : "";
|
||||
}
|
||||
|
||||
/** Builds run_subagent's BuiltinTool from tool config + injected services. */
|
||||
export function createSubagentTool(
|
||||
definition: ToolDefinitionConfig,
|
||||
services?: EnvironmentServices,
|
||||
): BuiltinTool {
|
||||
const runner = services?.subagentRunner;
|
||||
const manager = services?.subagentSessions;
|
||||
return {
|
||||
name: SUBAGENT_NAME,
|
||||
definition,
|
||||
async *execute(
|
||||
args: Record<string, unknown>,
|
||||
ctx: ToolExecutionContext,
|
||||
): AsyncGenerator<OmniMessage, ToolResult | void> {
|
||||
const { toolCallId, signal, approve } = ctx;
|
||||
const fail = function* (msg: string): Generator<OmniMessage> {
|
||||
yield partialToolCallOutput({ eventType: "delta", output: msg, toolCallId });
|
||||
};
|
||||
|
||||
// Missing arguments / unconfigured services both collapse to an explanatory output rather
|
||||
// than throwing (consistent with other tools).
|
||||
if (!runner) {
|
||||
yield* fail("[run_subagent unavailable: no subagent runner configured]");
|
||||
return { stopReason: "failed" };
|
||||
}
|
||||
if (!manager || manager.isDisposed) {
|
||||
yield* fail("[run_subagent unavailable: no subagent session manager available]");
|
||||
return { stopReason: "failed" };
|
||||
}
|
||||
const prompt = typeof args.prompt === "string" ? args.prompt : "";
|
||||
if (prompt.trim().length === 0) {
|
||||
yield* fail("[run_subagent error: missing required string argument `prompt`]");
|
||||
return { stopReason: "failed" };
|
||||
}
|
||||
const agentId = typeof args.agent_id === "string" ? args.agent_id : undefined;
|
||||
const modelId = typeof args.model_id === "string" ? args.model_id : undefined;
|
||||
const yieldMs = clampYield(
|
||||
args.yield_time_ms,
|
||||
DEFAULT_SUBAGENT_YIELD_MS,
|
||||
definition.timeoutMs,
|
||||
);
|
||||
|
||||
if (signal?.aborted) return { stopReason: "aborted" };
|
||||
|
||||
// Concurrency cap (running child Agents are never evicted): reject spawning if there's no
|
||||
// room.
|
||||
if (!manager.makeRoom()) {
|
||||
yield* fail(
|
||||
"[run_subagent error: too many background subagents; poll or finish existing ones first]",
|
||||
);
|
||||
return { stopReason: "failed" };
|
||||
}
|
||||
|
||||
// Spawn the child Session (precheck errors such as exceeding the depth limit or a
|
||||
// nonexistent agent are expressed as a throw).
|
||||
let session: ManagedSubagentSession;
|
||||
try {
|
||||
const handle = await runner.spawn({
|
||||
...(agentId !== undefined ? { agentId } : {}),
|
||||
...(modelId !== undefined ? { modelId } : {}),
|
||||
});
|
||||
session = new ManagedSubagentSession(handle);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
yield* fail(`[run_subagent error: ${message}]`);
|
||||
return { stopReason: "failed" };
|
||||
}
|
||||
|
||||
// An interruption within the startup window kills the child session (consistent with
|
||||
// exec_command); once switched to background, this listener is removed in `finally`.
|
||||
const onAbort = (): void => session.kill();
|
||||
let registered = false;
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
try {
|
||||
session.startRun(prompt);
|
||||
yield* collectWindow(session, {
|
||||
yieldMs,
|
||||
toolCallId,
|
||||
...(signal ? { signal } : {}),
|
||||
...(approve ? { approve } : {}),
|
||||
});
|
||||
|
||||
if (signal?.aborted) return { stopReason: "aborted" };
|
||||
if (session.running) {
|
||||
// Still running once the window expires: register as a background session, returning
|
||||
// subagent_id for input_subagent to continue accessing it.
|
||||
const id = manager.register(session);
|
||||
registered = true;
|
||||
return {
|
||||
stopReason: "completed",
|
||||
note:
|
||||
`[subagent running with subagent_id ${id}; use input_subagent to poll for progress ` +
|
||||
`or send a follow-up prompt]` +
|
||||
approvalHint(session),
|
||||
};
|
||||
}
|
||||
// Finished within the window: report the terminal state; releasing the child session is
|
||||
// handled uniformly in `finally` (never registered, so no subagent_id).
|
||||
return resultForSubagentExit(session.exit);
|
||||
} finally {
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
if (!registered) session.kill();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user