@oh-my-pi/pi-agent-core 18.1.5 → 18.1.6
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.
- package/CHANGELOG.md +10 -0
- package/dist/types/types.d.ts +36 -7
- package/package.json +8 -8
- package/src/agent-loop.ts +80 -15
- package/src/compaction/compaction-v2-streaming.ts +1 -1
- package/src/types.ts +34 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.1.6] - 2026-09-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added support for streaming tool argument updates, providing more responsive tool-call progress.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Improved steering behavior so queued steering commands preserve already-emitted non-interruptible tool calls while aborting or skipping only interruptible waits.
|
|
14
|
+
|
|
5
15
|
## [18.1.2] - 2026-09-01
|
|
6
16
|
|
|
7
17
|
### Fixed
|
package/dist/types/types.d.ts
CHANGED
|
@@ -195,8 +195,10 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
195
195
|
/**
|
|
196
196
|
* Peeks whether steering messages are queued, without consuming them.
|
|
197
197
|
*
|
|
198
|
-
*
|
|
199
|
-
* whether to
|
|
198
|
+
* Polled while a tool batch runs (unless interruptMode is "wait") to decide
|
|
199
|
+
* whether to abort in-flight and skip not-yet-started *interruptible* waits;
|
|
200
|
+
* every other already-emitted call still executes and the message injects
|
|
201
|
+
* at the batch boundary. The queue keeps
|
|
200
202
|
* owning its messages until the loop reaches the next injection boundary and
|
|
201
203
|
* dequeues via {@link getSteeringMessages} — so callers can still cancel or
|
|
202
204
|
* restore queued messages while in-flight tools settle, and an external
|
|
@@ -653,8 +655,29 @@ export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprov
|
|
|
653
655
|
export interface AgentToolContext {
|
|
654
656
|
}
|
|
655
657
|
export type AgentToolExecFn<TParameters extends TSchema = TSchema, TDetails = any, TTheme = unknown> = (this: AgentTool<TParameters, TDetails, TTheme>, toolCallId: string, params: Static<TParameters>, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>, context?: AgentToolContext) => Promise<AgentToolResult<TDetails, TParameters>>;
|
|
658
|
+
/** Live receiver for a tool call's streamed arguments (see AgentTool.openArgStream). */
|
|
659
|
+
export interface AgentToolArgStream {
|
|
660
|
+
/** Raw wire fragment of the arguments (JSON text, or verbatim payload for custom-format tools). */
|
|
661
|
+
push(delta: string): void;
|
|
662
|
+
/** Arguments are complete; `args` is the final parsed object the loop will pass to `execute`. */
|
|
663
|
+
end(args: unknown): void;
|
|
664
|
+
/** The call will never execute (stream error, abort, blocked). Release resources. */
|
|
665
|
+
cancel(): void;
|
|
666
|
+
}
|
|
667
|
+
export interface AgentToolArgStreamInit {
|
|
668
|
+
toolCallId: string;
|
|
669
|
+
toolName: string;
|
|
670
|
+
/** Wire-level name for custom-format tools; undefined for JSON function tools. */
|
|
671
|
+
customWireName?: string;
|
|
672
|
+
/** Push a serializable projection of the in-flight call (e.g. diff previews); surfaces as `tool_stream_update`. */
|
|
673
|
+
emit(update: unknown): void;
|
|
674
|
+
}
|
|
656
675
|
export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any, TTheme = unknown> extends Tool<TParameters> {
|
|
657
676
|
label: string;
|
|
677
|
+
/**
|
|
678
|
+
* Called at `toolcall_start`, before any argument delta. Return `undefined` to opt out.
|
|
679
|
+
*/
|
|
680
|
+
openArgStream?: (init: AgentToolArgStreamInit) => AgentToolArgStream | undefined;
|
|
658
681
|
/** If true, tool is excluded unless explicitly listed in --tools or agent's tools field */
|
|
659
682
|
hidden?: boolean;
|
|
660
683
|
/** If true, tool can stage a pending action that requires explicit resolution via the resolve tool. */
|
|
@@ -673,14 +696,15 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
|
|
|
673
696
|
/** If true, argument validation errors are non-fatal: raw args are passed to execute() instead of returning an error to the LLM. */
|
|
674
697
|
lenientArgValidation?: boolean;
|
|
675
698
|
/**
|
|
676
|
-
* Whether the agent loop may abort this tool mid-execution
|
|
677
|
-
*
|
|
678
|
-
* pre-validation arguments.
|
|
699
|
+
* Whether the agent loop may abort this tool mid-execution — or skip it
|
|
700
|
+
* before it starts — to deliver a queued steering message. A function
|
|
701
|
+
* resolves this per call from the raw, pre-validation arguments.
|
|
679
702
|
*
|
|
680
703
|
* Enable only for calls that purely *wait* and observe their abort signal
|
|
681
704
|
* cleanly (e.g. `job` poll), so the abort surfaces the tool's current
|
|
682
|
-
* snapshot rather than corrupting a side effect.
|
|
683
|
-
*
|
|
705
|
+
* snapshot rather than corrupting a side effect. Every other call runs to
|
|
706
|
+
* completion even when steering is queued; the message lands at the next
|
|
707
|
+
* batch boundary. Honored only when `interruptMode` is "immediate".
|
|
684
708
|
*/
|
|
685
709
|
interruptible?: boolean | ((args: Partial<Static<TParameters>>) => boolean);
|
|
686
710
|
/**
|
|
@@ -777,6 +801,11 @@ export type AgentEvent = {
|
|
|
777
801
|
toolName: string;
|
|
778
802
|
args: any;
|
|
779
803
|
partialResult: any;
|
|
804
|
+
} | {
|
|
805
|
+
type: "tool_stream_update";
|
|
806
|
+
toolCallId: string;
|
|
807
|
+
toolName: string;
|
|
808
|
+
update: unknown;
|
|
780
809
|
} | {
|
|
781
810
|
type: "tool_execution_end";
|
|
782
811
|
toolCallId: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-agent-core",
|
|
4
|
-
"version": "18.1.
|
|
4
|
+
"version": "18.1.6",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -35,16 +35,16 @@
|
|
|
35
35
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "18.1.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "18.1.
|
|
40
|
-
"@oh-my-pi/pi-natives": "18.1.
|
|
41
|
-
"@oh-my-pi/pi-utils": "18.1.
|
|
42
|
-
"@oh-my-pi/pi-wire": "18.1.
|
|
43
|
-
"@oh-my-pi/snapcompact": "18.1.
|
|
38
|
+
"@oh-my-pi/pi-ai": "18.1.6",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "18.1.6",
|
|
40
|
+
"@oh-my-pi/pi-natives": "18.1.6",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.1.6",
|
|
42
|
+
"@oh-my-pi/pi-wire": "18.1.6",
|
|
43
|
+
"@oh-my-pi/snapcompact": "18.1.6",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@oh-my-pi/omptype": "18.1.
|
|
47
|
+
"@oh-my-pi/omptype": "18.1.6",
|
|
48
48
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
49
49
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
50
50
|
"@types/bun": "^1.3.14"
|
package/src/agent-loop.ts
CHANGED
|
@@ -73,6 +73,7 @@ import type {
|
|
|
73
73
|
AgentMessage,
|
|
74
74
|
AgentPreModelCallResult,
|
|
75
75
|
AgentTool,
|
|
76
|
+
AgentToolArgStream,
|
|
76
77
|
AgentToolCall,
|
|
77
78
|
AgentToolResult,
|
|
78
79
|
AgentTurnEndContext,
|
|
@@ -924,7 +925,10 @@ function extractIntent(args: Record<string, unknown>): { intent?: string; stripp
|
|
|
924
925
|
if (typeof intent !== "string") {
|
|
925
926
|
return { strippedArgs };
|
|
926
927
|
}
|
|
927
|
-
const trimmed = intent
|
|
928
|
+
const trimmed = intent
|
|
929
|
+
.trim()
|
|
930
|
+
.replace(/\s*\.+$/, "")
|
|
931
|
+
.trim();
|
|
928
932
|
return { intent: trimmed.length > 0 ? trimmed : undefined, strippedArgs };
|
|
929
933
|
}
|
|
930
934
|
|
|
@@ -1775,6 +1779,17 @@ async function streamAssistantResponse(
|
|
|
1775
1779
|
let partialMessage: AssistantMessage | null = null;
|
|
1776
1780
|
let addedPartial = false;
|
|
1777
1781
|
const completedToolCallIds = new Set<string>();
|
|
1782
|
+
const argStreams = new Map<number, { id: string; stream: AgentToolArgStream }>();
|
|
1783
|
+
const cancelArgStreams = (): void => {
|
|
1784
|
+
for (const { id, stream: argStream } of argStreams.values()) {
|
|
1785
|
+
try {
|
|
1786
|
+
argStream.cancel();
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
logger.debug("Tool argument stream cancel failed", { toolCallId: id, error });
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
argStreams.clear();
|
|
1792
|
+
};
|
|
1778
1793
|
|
|
1779
1794
|
const responseIterator = response[Symbol.asyncIterator]();
|
|
1780
1795
|
const finishAbortedStream = async (): Promise<AssistantMessage> => {
|
|
@@ -1888,6 +1903,52 @@ async function streamAssistantResponse(
|
|
|
1888
1903
|
// when the LLM is streaming chunks faster than the loop can rest.
|
|
1889
1904
|
await yieldIfDue();
|
|
1890
1905
|
|
|
1906
|
+
if (event.type === "toolcall_start") {
|
|
1907
|
+
const block = event.partial.content[event.contentIndex];
|
|
1908
|
+
if (block?.type === "toolCall") {
|
|
1909
|
+
const tool = resolveToolForCall(context.tools, block, config.resolveFallbackTool);
|
|
1910
|
+
if (tool?.openArgStream) {
|
|
1911
|
+
try {
|
|
1912
|
+
const argStream = tool.openArgStream({
|
|
1913
|
+
toolCallId: block.id,
|
|
1914
|
+
toolName: block.name,
|
|
1915
|
+
customWireName: block.customWireName,
|
|
1916
|
+
emit: update =>
|
|
1917
|
+
stream.push({
|
|
1918
|
+
type: "tool_stream_update",
|
|
1919
|
+
toolCallId: block.id,
|
|
1920
|
+
toolName: block.name,
|
|
1921
|
+
update,
|
|
1922
|
+
}),
|
|
1923
|
+
});
|
|
1924
|
+
if (argStream) argStreams.set(event.contentIndex, { id: block.id, stream: argStream });
|
|
1925
|
+
} catch (error) {
|
|
1926
|
+
logger.debug("Tool argument stream open failed", { toolCallId: block.id, error });
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
} else if (event.type === "toolcall_delta") {
|
|
1931
|
+
const entry = argStreams.get(event.contentIndex);
|
|
1932
|
+
if (entry) {
|
|
1933
|
+
try {
|
|
1934
|
+
entry.stream.push(event.delta);
|
|
1935
|
+
} catch (error) {
|
|
1936
|
+
logger.debug("Tool argument stream push failed", { toolCallId: entry.id, error });
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
} else if (event.type === "toolcall_end") {
|
|
1940
|
+
const entry = argStreams.get(event.contentIndex);
|
|
1941
|
+
if (entry) {
|
|
1942
|
+
try {
|
|
1943
|
+
entry.stream.end(event.toolCall.arguments);
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
logger.debug("Tool argument stream end failed", { toolCallId: entry.id, error });
|
|
1946
|
+
} finally {
|
|
1947
|
+
argStreams.delete(event.contentIndex);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1891
1952
|
switch (event.type) {
|
|
1892
1953
|
case "start":
|
|
1893
1954
|
partialMessage = event.partial;
|
|
@@ -1944,6 +2005,7 @@ async function streamAssistantResponse(
|
|
|
1944
2005
|
}
|
|
1945
2006
|
} finally {
|
|
1946
2007
|
detachAbortListener?.();
|
|
2008
|
+
cancelArgStreams();
|
|
1947
2009
|
}
|
|
1948
2010
|
|
|
1949
2011
|
let trailing = await response.result();
|
|
@@ -2440,8 +2502,9 @@ async function executeToolCalls(
|
|
|
2440
2502
|
// Queued steering hard-aborts only interruptible waits and raises the
|
|
2441
2503
|
// cooperative soft signal for everything else: the boundary dequeue
|
|
2442
2504
|
// below injects the message as soon as running tools finish (or
|
|
2443
|
-
// background themselves), and not-yet-started
|
|
2444
|
-
// Idempotent — a second steer poll after the abort is
|
|
2505
|
+
// background themselves), and not-yet-started interruptible waits
|
|
2506
|
+
// are skipped. Idempotent — a second steer poll after the abort is
|
|
2507
|
+
// a no-op.
|
|
2445
2508
|
if (!steeringAbortController.signal.aborted) {
|
|
2446
2509
|
interruptState.triggered = true;
|
|
2447
2510
|
interruptState.source = steeringSource ?? "unknown";
|
|
@@ -2495,16 +2558,18 @@ async function executeToolCalls(
|
|
|
2495
2558
|
};
|
|
2496
2559
|
|
|
2497
2560
|
const runTool = async (record: (typeof records)[number], index: number): Promise<void> => {
|
|
2498
|
-
// A pending interrupt preempts not-yet-started
|
|
2499
|
-
// injects promptly
|
|
2500
|
-
// interruptible
|
|
2501
|
-
//
|
|
2502
|
-
//
|
|
2503
|
-
//
|
|
2504
|
-
// `todo`/`write`
|
|
2505
|
-
//
|
|
2506
|
-
// still
|
|
2507
|
-
|
|
2561
|
+
// A pending interrupt preempts not-yet-started *interruptible* waits so
|
|
2562
|
+
// the message injects promptly instead of sitting out a `hub wait`.
|
|
2563
|
+
// Non-interruptible work is never skipped, whatever the source: the
|
|
2564
|
+
// expensive part — generating the call — is already paid, the tool
|
|
2565
|
+
// itself is cheap, and a skip only makes the model re-emit the same
|
|
2566
|
+
// call after the steer lands (#10439). The same guarantee is what keeps
|
|
2567
|
+
// a batched `todo`/`write` queued behind an aborted wait alive (#7493)
|
|
2568
|
+
// and lets a subagent's already-emitted terminal `yield` commit when
|
|
2569
|
+
// the parent steers mid-stream (#10645). The steer still injects at the
|
|
2570
|
+
// batch boundary; the cooperative soft signal lets long-running tools
|
|
2571
|
+
// step aside on their own.
|
|
2572
|
+
if (interruptState.triggered && record.interruptible) {
|
|
2508
2573
|
// Skip both span emission and the collector orphan record here. The
|
|
2509
2574
|
// tail sweep below (after `Promise.allSettled`) is the single path
|
|
2510
2575
|
// that handles "no result message was produced" — it calls
|
|
@@ -2719,8 +2784,8 @@ async function executeToolCalls(
|
|
|
2719
2784
|
|
|
2720
2785
|
// While tool calls are in flight, queued steering or interrupting IRC would
|
|
2721
2786
|
// otherwise wait out the tools' own window. Poll only non-consuming queues:
|
|
2722
|
-
// detection hard-aborts interruptible waits
|
|
2723
|
-
// (auto-background bash),
|
|
2787
|
+
// detection hard-aborts interruptible waits (running or not yet started)
|
|
2788
|
+
// and soft-signals cooperative tools (auto-background bash), so the boundary
|
|
2724
2789
|
// dequeue below injects the message promptly. Gated on immediate-interrupt
|
|
2725
2790
|
// mode; checkSteering is idempotent (no-op once triggered).
|
|
2726
2791
|
const watchSteeringWhileRunning =
|
|
@@ -407,7 +407,7 @@ function buildCompactionV2Headers(
|
|
|
407
407
|
"content-type": "application/json",
|
|
408
408
|
...resolveOpenAIRequestSetup(
|
|
409
409
|
{ provider: model.provider, id: model.id, baseUrl: model.baseUrl, headers: model.headers },
|
|
410
|
-
{ apiKey, messages: [],
|
|
410
|
+
{ apiKey, messages: [], sessionId: request.sessionId ?? routingSessionId, promptCacheSessionId },
|
|
411
411
|
).headers,
|
|
412
412
|
};
|
|
413
413
|
if (api === "openai-codex-responses" || model.provider === "openai-codex") {
|
package/src/types.ts
CHANGED
|
@@ -245,8 +245,10 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
245
245
|
/**
|
|
246
246
|
* Peeks whether steering messages are queued, without consuming them.
|
|
247
247
|
*
|
|
248
|
-
*
|
|
249
|
-
* whether to
|
|
248
|
+
* Polled while a tool batch runs (unless interruptMode is "wait") to decide
|
|
249
|
+
* whether to abort in-flight and skip not-yet-started *interruptible* waits;
|
|
250
|
+
* every other already-emitted call still executes and the message injects
|
|
251
|
+
* at the batch boundary. The queue keeps
|
|
250
252
|
* owning its messages until the loop reaches the next injection boundary and
|
|
251
253
|
* dequeues via {@link getSteeringMessages} — so callers can still cancel or
|
|
252
254
|
* restore queued messages while in-flight tools settle, and an external
|
|
@@ -758,6 +760,25 @@ export type AgentToolExecFn<TParameters extends TSchema = TSchema, TDetails = an
|
|
|
758
760
|
context?: AgentToolContext,
|
|
759
761
|
) => Promise<AgentToolResult<TDetails, TParameters>>;
|
|
760
762
|
|
|
763
|
+
/** Live receiver for a tool call's streamed arguments (see AgentTool.openArgStream). */
|
|
764
|
+
export interface AgentToolArgStream {
|
|
765
|
+
/** Raw wire fragment of the arguments (JSON text, or verbatim payload for custom-format tools). */
|
|
766
|
+
push(delta: string): void;
|
|
767
|
+
/** Arguments are complete; `args` is the final parsed object the loop will pass to `execute`. */
|
|
768
|
+
end(args: unknown): void;
|
|
769
|
+
/** The call will never execute (stream error, abort, blocked). Release resources. */
|
|
770
|
+
cancel(): void;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export interface AgentToolArgStreamInit {
|
|
774
|
+
toolCallId: string;
|
|
775
|
+
toolName: string;
|
|
776
|
+
/** Wire-level name for custom-format tools; undefined for JSON function tools. */
|
|
777
|
+
customWireName?: string;
|
|
778
|
+
/** Push a serializable projection of the in-flight call (e.g. diff previews); surfaces as `tool_stream_update`. */
|
|
779
|
+
emit(update: unknown): void;
|
|
780
|
+
}
|
|
781
|
+
|
|
761
782
|
// AgentTool extends Tool but adds the execute function
|
|
762
783
|
export interface AgentTool<
|
|
763
784
|
TParameters extends TSchema = TSchema,
|
|
@@ -766,6 +787,10 @@ export interface AgentTool<
|
|
|
766
787
|
> extends Tool<TParameters> {
|
|
767
788
|
// A human-readable label for the tool to be displayed in UI
|
|
768
789
|
label: string;
|
|
790
|
+
/**
|
|
791
|
+
* Called at `toolcall_start`, before any argument delta. Return `undefined` to opt out.
|
|
792
|
+
*/
|
|
793
|
+
openArgStream?: (init: AgentToolArgStreamInit) => AgentToolArgStream | undefined;
|
|
769
794
|
/** If true, tool is excluded unless explicitly listed in --tools or agent's tools field */
|
|
770
795
|
hidden?: boolean;
|
|
771
796
|
/** If true, tool can stage a pending action that requires explicit resolution via the resolve tool. */
|
|
@@ -784,14 +809,15 @@ export interface AgentTool<
|
|
|
784
809
|
/** If true, argument validation errors are non-fatal: raw args are passed to execute() instead of returning an error to the LLM. */
|
|
785
810
|
lenientArgValidation?: boolean;
|
|
786
811
|
/**
|
|
787
|
-
* Whether the agent loop may abort this tool mid-execution
|
|
788
|
-
*
|
|
789
|
-
* pre-validation arguments.
|
|
812
|
+
* Whether the agent loop may abort this tool mid-execution — or skip it
|
|
813
|
+
* before it starts — to deliver a queued steering message. A function
|
|
814
|
+
* resolves this per call from the raw, pre-validation arguments.
|
|
790
815
|
*
|
|
791
816
|
* Enable only for calls that purely *wait* and observe their abort signal
|
|
792
817
|
* cleanly (e.g. `job` poll), so the abort surfaces the tool's current
|
|
793
|
-
* snapshot rather than corrupting a side effect.
|
|
794
|
-
*
|
|
818
|
+
* snapshot rather than corrupting a side effect. Every other call runs to
|
|
819
|
+
* completion even when steering is queued; the message lands at the next
|
|
820
|
+
* batch boundary. Honored only when `interruptMode` is "immediate".
|
|
795
821
|
*/
|
|
796
822
|
interruptible?: boolean | ((args: Partial<Static<TParameters>>) => boolean);
|
|
797
823
|
/**
|
|
@@ -885,4 +911,5 @@ export type AgentEvent =
|
|
|
885
911
|
// Tool execution lifecycle
|
|
886
912
|
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any; intent?: string }
|
|
887
913
|
| { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any }
|
|
914
|
+
| { type: "tool_stream_update"; toolCallId: string; toolName: string; update: unknown }
|
|
888
915
|
| { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError?: boolean };
|