chore(vendor): import pi 0.85.1
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
175
third_party/pi/packages/agent/src/harness/execution/assistant.ts
vendored
Normal file
175
third_party/pi/packages/agent/src/harness/execution/assistant.ts
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
import type {
|
||||
Context as AiContext,
|
||||
Api,
|
||||
AssistantMessage,
|
||||
AssistantMessageEvent,
|
||||
AssistantMessageEventStream,
|
||||
Message,
|
||||
Model,
|
||||
SimpleStreamOptions,
|
||||
Tool,
|
||||
} from "@earendil-works/pi-ai";
|
||||
import type { AgentMessage, ThinkingLevel } from "../../types.ts";
|
||||
import { type Context, getTelemetryContext } from "../context.ts";
|
||||
import type { SettledAssistantMessage } from "../session/types.ts";
|
||||
import type { AgentHarnessStreamOptions } from "../types.ts";
|
||||
import { AbortRequested } from "./effect-gate.ts";
|
||||
|
||||
/** HTTP response metadata captured before the provider response body is consumed. */
|
||||
export interface AssistantResponseMetadata {
|
||||
status?: number;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
/** Process-local lifecycle observer for one assistant stream. */
|
||||
export interface AssistantStreamObserver {
|
||||
start(
|
||||
message: AssistantMessage,
|
||||
event: Extract<AssistantMessageEvent, { type: "start" }>,
|
||||
context: Context,
|
||||
): void | Promise<void>;
|
||||
update(message: AssistantMessage, event: AssistantMessageEvent, context: Context): void | Promise<void>;
|
||||
end(message: SettledAssistantMessage, context: Context): void | Promise<void>;
|
||||
}
|
||||
|
||||
/** Executable inputs for one already-approved assistant provider request. */
|
||||
export interface HarnessAssistantStreamConfig {
|
||||
model: Model<Api>;
|
||||
systemPrompt: string;
|
||||
tools?: Tool[];
|
||||
thinkingLevel: ThinkingLevel;
|
||||
streamOptions: AgentHarnessStreamOptions;
|
||||
transformContext?: (
|
||||
requestContext: { messages: AgentMessage[]; systemPrompt: string },
|
||||
context: Context,
|
||||
) => Promise<{ messages: AgentMessage[]; systemPrompt: string }>;
|
||||
toProviderMessages: (messages: AgentMessage[], context: Context) => Message[] | Promise<Message[]>;
|
||||
beforePayload?: (
|
||||
payload: unknown,
|
||||
model: Model<Api>,
|
||||
context: Context,
|
||||
) => unknown | undefined | Promise<unknown | undefined>;
|
||||
afterResponse?: (
|
||||
message: SettledAssistantMessage,
|
||||
metadata: AssistantResponseMetadata,
|
||||
context: Context,
|
||||
) => Promise<SettledAssistantMessage>;
|
||||
request(
|
||||
aiContext: AiContext,
|
||||
options: SimpleStreamOptions,
|
||||
context: Context,
|
||||
): AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
|
||||
observer: AssistantStreamObserver;
|
||||
}
|
||||
|
||||
function createRequestOptions(
|
||||
config: HarnessAssistantStreamConfig,
|
||||
captureMetadata: (metadata: AssistantResponseMetadata) => void,
|
||||
context: Context,
|
||||
): SimpleStreamOptions {
|
||||
const options = config.streamOptions;
|
||||
return {
|
||||
transport: options.transport,
|
||||
timeoutMs: options.timeoutMs,
|
||||
maxRetries: options.maxRetries,
|
||||
maxRetryDelayMs: options.maxRetryDelayMs,
|
||||
headers: options.headers,
|
||||
metadata: options.metadata,
|
||||
cacheRetention: options.cacheRetention,
|
||||
deferred: options.deferred,
|
||||
...(config.thinkingLevel === "off" ? {} : { reasoning: config.thinkingLevel }),
|
||||
signal: context.abortSignal,
|
||||
telemetryContext: getTelemetryContext(context),
|
||||
onPayload:
|
||||
config.beforePayload === undefined
|
||||
? undefined
|
||||
: (payload, model) => config.beforePayload?.(payload, model, context),
|
||||
onResponse: (response) => {
|
||||
captureMetadata({ status: response.status, headers: response.headers });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isUpdateEvent(
|
||||
event: AssistantMessageEvent,
|
||||
): event is Exclude<AssistantMessageEvent, { type: "start" | "done" | "error" }> {
|
||||
return event.type !== "start" && event.type !== "done" && event.type !== "error";
|
||||
}
|
||||
|
||||
export async function consumeAssistantStream(
|
||||
stream: AssistantMessageEventStream,
|
||||
observer: AssistantStreamObserver,
|
||||
afterResponse:
|
||||
| ((message: SettledAssistantMessage, context: Context) => Promise<SettledAssistantMessage>)
|
||||
| undefined,
|
||||
context: Context,
|
||||
): Promise<SettledAssistantMessage> {
|
||||
let started = false;
|
||||
for await (const event of stream) {
|
||||
if (event.type === "start") {
|
||||
if (started) throw new Error("Assistant message stream emitted more than one start event");
|
||||
started = true;
|
||||
await observer.start({ ...event.partial }, event, context);
|
||||
} else if (isUpdateEvent(event)) {
|
||||
if (!started) throw new Error(`Assistant message stream emitted ${event.type} before start`);
|
||||
await observer.update({ ...event.partial }, event, context);
|
||||
} else if (event.type === "done" && !started) {
|
||||
throw new Error("Assistant message stream emitted done before start");
|
||||
}
|
||||
}
|
||||
|
||||
const settled = (await stream.result()) as SettledAssistantMessage;
|
||||
let finalMessage = settled;
|
||||
if (afterResponse !== undefined) {
|
||||
try {
|
||||
finalMessage = await afterResponse(settled, context);
|
||||
} catch (error) {
|
||||
if (!(error instanceof AbortRequested)) throw error;
|
||||
await error.cancellation;
|
||||
}
|
||||
}
|
||||
await observer.end(finalMessage, context);
|
||||
return finalMessage;
|
||||
}
|
||||
|
||||
/** Stream one assistant response without mutating the caller's message list. */
|
||||
export async function streamHarnessAssistant(
|
||||
messages: AgentMessage[],
|
||||
config: HarnessAssistantStreamConfig,
|
||||
context: Context,
|
||||
): Promise<SettledAssistantMessage> {
|
||||
let requestContext = { messages: messages.slice(), systemPrompt: config.systemPrompt };
|
||||
if (config.transformContext) {
|
||||
requestContext = await config.transformContext(requestContext, context);
|
||||
}
|
||||
|
||||
const providerMessages = await config.toProviderMessages(requestContext.messages, context);
|
||||
const aiContext: AiContext = {
|
||||
systemPrompt: requestContext.systemPrompt,
|
||||
messages: providerMessages,
|
||||
tools: config.tools,
|
||||
};
|
||||
|
||||
let metadata: AssistantResponseMetadata = {};
|
||||
const stream = await config.request(
|
||||
aiContext,
|
||||
createRequestOptions(
|
||||
config,
|
||||
(nextMetadata) => {
|
||||
metadata = nextMetadata;
|
||||
},
|
||||
context,
|
||||
),
|
||||
context,
|
||||
);
|
||||
|
||||
const afterResponse = config.afterResponse;
|
||||
return consumeAssistantStream(
|
||||
stream,
|
||||
config.observer,
|
||||
afterResponse === undefined
|
||||
? undefined
|
||||
: (message, afterContext) => afterResponse(message, metadata, afterContext),
|
||||
context,
|
||||
);
|
||||
}
|
||||
64
third_party/pi/packages/agent/src/harness/execution/effect-gate.ts
vendored
Normal file
64
third_party/pi/packages/agent/src/harness/execution/effect-gate.ts
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
/** Expected internal control flow when cancellation wins effect admission. */
|
||||
export class AbortRequested extends Error {
|
||||
readonly cancellation: Promise<void>;
|
||||
|
||||
constructor(cancellation: Promise<void>) {
|
||||
super("Abort requested");
|
||||
this.name = "AbortRequested";
|
||||
this.cancellation = cancellation;
|
||||
}
|
||||
}
|
||||
|
||||
/** Procedure-facing synchronous admission capability for one drive pass. */
|
||||
export interface Gate {
|
||||
readonly signal: AbortSignal;
|
||||
admit<T>(invoke: () => T): T;
|
||||
}
|
||||
|
||||
/** Owner-facing lifecycle controls for one drive pass. */
|
||||
export interface GateControl {
|
||||
beginAbort(cancellation: Promise<void>): void;
|
||||
signalAbort(): void;
|
||||
close(error: Error): void;
|
||||
}
|
||||
|
||||
type GateState =
|
||||
| { status: "open" }
|
||||
| { status: "aborting"; cancellation: Promise<void> }
|
||||
| { status: "closed"; error: Error };
|
||||
|
||||
/** Create separate procedure-facing and owner-facing views of one effect gate. */
|
||||
export function createGate(): { gate: Gate; control: GateControl } {
|
||||
let state: GateState = { status: "open" };
|
||||
const controller = new AbortController();
|
||||
|
||||
const check = (): void => {
|
||||
if (state.status === "aborting") throw new AbortRequested(state.cancellation);
|
||||
if (state.status === "closed") throw state.error;
|
||||
};
|
||||
|
||||
return {
|
||||
gate: {
|
||||
admit<T>(invoke: () => T): T {
|
||||
check();
|
||||
return invoke();
|
||||
},
|
||||
signal: controller.signal,
|
||||
},
|
||||
control: {
|
||||
beginAbort(cancellation) {
|
||||
if (state.status !== "open") return;
|
||||
state = { status: "aborting", cancellation };
|
||||
},
|
||||
signalAbort() {
|
||||
if (state.status !== "aborting" || controller.signal.aborted) return;
|
||||
controller.abort(new AbortRequested(state.cancellation));
|
||||
},
|
||||
close(error) {
|
||||
if (state.status === "closed") return;
|
||||
state = { status: "closed", error };
|
||||
if (!controller.signal.aborted) controller.abort(error);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
210
third_party/pi/packages/agent/src/harness/execution/tools.ts
vendored
Normal file
210
third_party/pi/packages/agent/src/harness/execution/tools.ts
vendored
Normal file
@@ -0,0 +1,210 @@
|
||||
import { type ToolResultMessage, validateToolArguments } from "@earendil-works/pi-ai";
|
||||
import type { AgentToolCall, AgentToolResult } from "../../types.ts";
|
||||
import { type Context, withAbortSignal } from "../context.ts";
|
||||
import type { JsonValue } from "../session/types.ts";
|
||||
import type { AgentHarnessTool, AgentHarnessToolInvocation, AgentHarnessToolUpdateCallback } from "../types.ts";
|
||||
import type { Gate } from "./effect-gate.ts";
|
||||
|
||||
/** A tool call whose tool exists and whose prepared arguments passed validation. */
|
||||
export interface PreparedToolCall<TContext extends object | undefined> {
|
||||
toolCall: AgentToolCall;
|
||||
tool: AgentHarnessTool<TContext>;
|
||||
args: Record<string, JsonValue>;
|
||||
}
|
||||
|
||||
/** Synthetic result produced without crossing the external tool-effect boundary. */
|
||||
export interface ImmediateToolOutcome {
|
||||
kind: "immediate";
|
||||
toolCall: AgentToolCall;
|
||||
result: AgentToolResult<unknown>;
|
||||
isError: true;
|
||||
terminate: boolean;
|
||||
}
|
||||
|
||||
/** Aggregated decision from the before-tool hook pipeline. */
|
||||
export interface BeforeToolDecision {
|
||||
args?: Record<string, JsonValue>;
|
||||
block?: { reason: string; terminate?: boolean };
|
||||
}
|
||||
|
||||
/** A prepared call cleared for durable intent publication and execution. */
|
||||
export interface ClearedToolCall<TContext extends object | undefined> {
|
||||
toolCall: AgentToolCall;
|
||||
tool: AgentHarnessTool<TContext>;
|
||||
args: Record<string, JsonValue>;
|
||||
}
|
||||
|
||||
/** Raw phase-two tool output before after-tool patching. */
|
||||
export interface ExecutedToolCall {
|
||||
result: AgentToolResult<unknown>;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
/** Aggregated patch from the after-tool hook pipeline. */
|
||||
export interface AfterToolPatch {
|
||||
content?: AgentToolResult<unknown>["content"];
|
||||
details?: JsonValue;
|
||||
isError?: boolean;
|
||||
usage?: AgentToolResult<unknown>["usage"];
|
||||
terminate?: boolean;
|
||||
}
|
||||
|
||||
/** Final tool output ready to become a durable tool-result message. */
|
||||
export interface FinalizedToolCall {
|
||||
toolCall: AgentToolCall;
|
||||
result: AgentToolResult<unknown>;
|
||||
isError: boolean;
|
||||
terminate: boolean;
|
||||
}
|
||||
|
||||
function createErrorToolResult(message: string): AgentToolResult<unknown> {
|
||||
return {
|
||||
content: [{ type: "text", text: message }],
|
||||
details: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function immediateError(toolCall: AgentToolCall, message: string, terminate = false): ImmediateToolOutcome {
|
||||
return {
|
||||
kind: "immediate",
|
||||
toolCall,
|
||||
result: createErrorToolResult(message),
|
||||
isError: true,
|
||||
terminate,
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve a tool, apply its deterministic argument preparation, and validate the result. */
|
||||
export function prepareToolCall<TContext extends object | undefined>(
|
||||
call: AgentToolCall,
|
||||
tools: AgentHarnessTool<TContext>[],
|
||||
): PreparedToolCall<TContext> | ImmediateToolOutcome {
|
||||
const tool = tools.find((candidate) => candidate.name === call.name);
|
||||
if (!tool) {
|
||||
return immediateError(call, `Tool ${JSON.stringify(call.name)} is unavailable`);
|
||||
}
|
||||
|
||||
try {
|
||||
const preparedArguments = tool.prepareArguments ? tool.prepareArguments(call.arguments) : call.arguments;
|
||||
const preparedCall: AgentToolCall =
|
||||
preparedArguments === call.arguments
|
||||
? call
|
||||
: { ...call, arguments: preparedArguments as Record<string, JsonValue> };
|
||||
const args = validateToolArguments(tool, preparedCall) as Record<string, JsonValue>;
|
||||
return { toolCall: call, tool, args };
|
||||
} catch (error) {
|
||||
return immediateError(call, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply an explicit hook decision and revalidate replacement arguments. */
|
||||
export function applyBeforeToolDecision<TContext extends object | undefined>(
|
||||
prepared: PreparedToolCall<TContext>,
|
||||
decision: BeforeToolDecision | undefined,
|
||||
): ClearedToolCall<TContext> | ImmediateToolOutcome {
|
||||
if (decision?.block) {
|
||||
return immediateError(prepared.toolCall, decision.block.reason, decision.block.terminate === true);
|
||||
}
|
||||
|
||||
if (!decision?.args) {
|
||||
return { toolCall: prepared.toolCall, tool: prepared.tool, args: prepared.args };
|
||||
}
|
||||
|
||||
try {
|
||||
const validatedArgs = validateToolArguments(prepared.tool, {
|
||||
...prepared.toolCall,
|
||||
arguments: decision.args,
|
||||
}) as Record<string, JsonValue>;
|
||||
return { toolCall: prepared.toolCall, tool: prepared.tool, args: validatedArgs };
|
||||
} catch (error) {
|
||||
return immediateError(prepared.toolCall, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
/** Execute one cleared external tool effect, converting expected tool throws to error output. */
|
||||
export function executeToolCall<TContext extends object | undefined>(
|
||||
call: ClearedToolCall<TContext>,
|
||||
gate: Gate,
|
||||
onUpdate: AgentHarnessToolUpdateCallback<unknown>,
|
||||
toolContext: TContext,
|
||||
invocation: AgentHarnessToolInvocation,
|
||||
context: Context,
|
||||
): Promise<ExecutedToolCall> {
|
||||
let acceptingUpdates = true;
|
||||
return gate.admit(async () => {
|
||||
const admittedContext = withAbortSignal(gate.signal, context);
|
||||
admittedContext.abortSignal?.throwIfAborted();
|
||||
try {
|
||||
const result = await call.tool.execute(
|
||||
call.toolCall.id,
|
||||
call.args,
|
||||
(partial, options) => {
|
||||
if (acceptingUpdates) onUpdate(partial, options);
|
||||
},
|
||||
toolContext,
|
||||
invocation,
|
||||
admittedContext,
|
||||
);
|
||||
return { result, isError: false };
|
||||
} catch (error) {
|
||||
return {
|
||||
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
||||
isError: true,
|
||||
};
|
||||
} finally {
|
||||
acceptingUpdates = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Apply an after-tool patch field by field. */
|
||||
export function finalizeToolCall<TContext extends object | undefined>(
|
||||
call: ClearedToolCall<TContext>,
|
||||
executed: ExecutedToolCall,
|
||||
patch: AfterToolPatch | undefined,
|
||||
): FinalizedToolCall {
|
||||
const result: AgentToolResult<unknown> = patch
|
||||
? {
|
||||
...executed.result,
|
||||
content: patch.content === undefined ? executed.result.content : patch.content,
|
||||
details: patch.details === undefined ? executed.result.details : patch.details,
|
||||
usage: patch.usage === undefined ? executed.result.usage : patch.usage,
|
||||
terminate: patch.terminate === undefined ? executed.result.terminate : patch.terminate,
|
||||
}
|
||||
: executed.result;
|
||||
return {
|
||||
toolCall: call.toolCall,
|
||||
result,
|
||||
isError: patch?.isError ?? executed.isError,
|
||||
terminate: result.terminate === true,
|
||||
};
|
||||
}
|
||||
|
||||
/** Reconstruct the canonical tool result represented by a staged transcript message. */
|
||||
export function toolResultFromMessage(
|
||||
message: ToolResultMessage<unknown>,
|
||||
terminate: boolean,
|
||||
): AgentToolResult<unknown> {
|
||||
return {
|
||||
content: message.content,
|
||||
details: message.details,
|
||||
...(message.usage === undefined ? {} : { usage: message.usage }),
|
||||
...(message.addedToolNames === undefined ? {} : { addedToolNames: message.addedToolNames }),
|
||||
...(terminate ? { terminate: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert finalized tool output to the provider-facing transcript message. */
|
||||
export function createToolResultMessage(call: FinalizedToolCall): ToolResultMessage {
|
||||
return {
|
||||
role: "toolResult",
|
||||
toolCallId: call.toolCall.id,
|
||||
toolName: call.toolCall.name,
|
||||
content: call.result.content ?? [],
|
||||
...(call.result.details === undefined ? {} : { details: call.result.details }),
|
||||
...(call.result.usage === undefined ? {} : { usage: call.result.usage }),
|
||||
...(call.result.addedToolNames?.length ? { addedToolNames: call.result.addedToolNames } : {}),
|
||||
isError: call.isError,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user