@iloveagents/foundry-agent 0.8.0 → 0.10.0
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/dist/client/agui-runner.d.ts +12 -0
- package/dist/client/agui-runner.js +73 -1
- package/dist/client/runner-events.d.ts +44 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/store/agent-state-store.d.ts +21 -0
- package/dist/store/agent-state-store.js +18 -0
- package/dist/store/reasoning-effort-store.d.ts +50 -4
- package/dist/store/reasoning-effort-store.js +35 -4
- package/package.json +1 -1
|
@@ -28,6 +28,17 @@ export interface AGUIRunnerOptions {
|
|
|
28
28
|
* three missed 15s server heartbeats. Pass `Infinity` to disable.
|
|
29
29
|
*/
|
|
30
30
|
stallAfterMs?: number;
|
|
31
|
+
/**
|
|
32
|
+
* When a run ends with a client-side tool call that never produced a
|
|
33
|
+
* result, synthesize an error result for it instead of leaving the call
|
|
34
|
+
* dangling. Defaults to `true`.
|
|
35
|
+
*
|
|
36
|
+
* Defence-in-depth for the failure this runner already had once: an
|
|
37
|
+
* unanswered tool call makes the model re-issue the tool and then report
|
|
38
|
+
* that it "didn't complete", while the Responses API rejects a replayed
|
|
39
|
+
* history whose `function_call` has no matching output.
|
|
40
|
+
*/
|
|
41
|
+
autoCancelPendingToolCalls?: boolean;
|
|
31
42
|
}
|
|
32
43
|
export interface AGUIRunInput {
|
|
33
44
|
/** AG-UI messages — caller is responsible for runtime-specific message conversion. */
|
|
@@ -50,6 +61,7 @@ export interface AGUIRunInput {
|
|
|
50
61
|
export declare class AGUIRunner {
|
|
51
62
|
private readonly httpAgent;
|
|
52
63
|
private readonly stallAfterMs;
|
|
64
|
+
private readonly autoCancelPendingToolCalls;
|
|
53
65
|
constructor(options?: AGUIRunnerOptions);
|
|
54
66
|
get threadId(): string;
|
|
55
67
|
get state(): unknown;
|
|
@@ -108,6 +108,7 @@ export class AGUIRunner {
|
|
|
108
108
|
...(options.fetchFn ? { fetch: options.fetchFn } : {}),
|
|
109
109
|
});
|
|
110
110
|
this.stallAfterMs = options.stallAfterMs ?? 45000;
|
|
111
|
+
this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls ?? true;
|
|
111
112
|
}
|
|
112
113
|
get threadId() {
|
|
113
114
|
return this.httpAgent.threadId;
|
|
@@ -151,7 +152,8 @@ export class AGUIRunner {
|
|
|
151
152
|
// The user's reasoning-effort choice rides on every turn as an
|
|
152
153
|
// AG-UI forwardedProp — read at request time so changing it
|
|
153
154
|
// mid-conversation takes effect on the next message. "default"
|
|
154
|
-
// sends nothing
|
|
155
|
+
// sends nothing, which is what makes the backend's own configured
|
|
156
|
+
// level apply; the picker shows the user what that resolves to.
|
|
155
157
|
const chosenEffort = reasoningEffortStore.getState().effort;
|
|
156
158
|
const forwardedProps = chosenEffort === "default" ? {} : { reasoningEffort: chosenEffort };
|
|
157
159
|
const runInputSnapshot = {
|
|
@@ -244,6 +246,56 @@ export class AGUIRunner {
|
|
|
244
246
|
onStepFinishedEvent: ({ event }) => {
|
|
245
247
|
push({ type: "step-finished", name: event.stepName });
|
|
246
248
|
},
|
|
249
|
+
// Reasoning has two nested boundaries: a BLOCK
|
|
250
|
+
// (REASONING_START…REASONING_END) may contain several MESSAGES
|
|
251
|
+
// (REASONING_MESSAGE_START…REASONING_MESSAGE_END). `reasoning-end`
|
|
252
|
+
// means "the model stopped thinking", so it is emitted only for the
|
|
253
|
+
// block terminal in `onReasoningEndEvent`. Emitting it per message
|
|
254
|
+
// too would fire it repeatedly and let consumers treat reasoning as
|
|
255
|
+
// finished while it is still going.
|
|
256
|
+
onReasoningStartEvent: () => {
|
|
257
|
+
push({ type: "streaming-status", status: { status: "reasoning" } });
|
|
258
|
+
},
|
|
259
|
+
// Server-published agent state. The AG-UI client has already
|
|
260
|
+
// applied the snapshot/patch to `httpAgent.state`; forward the
|
|
261
|
+
// result so consumers don't reach into the client.
|
|
262
|
+
onStateSnapshotEvent: () => {
|
|
263
|
+
push({ type: "agent-state", state: this.httpAgent.state });
|
|
264
|
+
},
|
|
265
|
+
onStateDeltaEvent: ({ event }) => {
|
|
266
|
+
push({
|
|
267
|
+
type: "agent-state",
|
|
268
|
+
state: this.httpAgent.state,
|
|
269
|
+
patch: event.delta,
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
// Server-authoritative history (e.g. after backend compaction).
|
|
273
|
+
// Deliberately does NOT touch the in-flight turn: upstream saw
|
|
274
|
+
// mid-run snapshots truncate a streaming answer.
|
|
275
|
+
onMessagesSnapshotEvent: ({ event }) => {
|
|
276
|
+
push({ type: "messages-snapshot", messages: event.messages });
|
|
277
|
+
},
|
|
278
|
+
// Generative-UI surfaces (MCP apps / A2UI), passed through.
|
|
279
|
+
onActivitySnapshotEvent: ({ event }) => {
|
|
280
|
+
push({
|
|
281
|
+
type: "activity",
|
|
282
|
+
messageId: event.messageId,
|
|
283
|
+
activityType: event.activityType,
|
|
284
|
+
content: event.content,
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
onActivityDeltaEvent: ({ event }) => {
|
|
288
|
+
push({
|
|
289
|
+
type: "activity",
|
|
290
|
+
messageId: event.messageId,
|
|
291
|
+
activityType: event.activityType,
|
|
292
|
+
patch: event.patch,
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
// Provider passthrough — never interpreted here.
|
|
296
|
+
onRawEvent: ({ event }) => {
|
|
297
|
+
push({ type: "raw", event: event.event, source: event.source });
|
|
298
|
+
},
|
|
247
299
|
onCustomEvent: ({ event }) => {
|
|
248
300
|
// Server heartbeat: liveness proof during long tool calls /
|
|
249
301
|
// thinking phases (also keeps intermediary idle-timeouts at bay
|
|
@@ -390,6 +442,26 @@ export class AGUIRunner {
|
|
|
390
442
|
abortSignal?.removeEventListener("abort", onAbort);
|
|
391
443
|
await runPromise; // ensure the run task is settled
|
|
392
444
|
}
|
|
445
|
+
// Any client-side tool that was STARTED but never produced a
|
|
446
|
+
// result leaves a `function_call` with no output. Settle it here so
|
|
447
|
+
// the replayed history stays valid and the model isn't left waiting
|
|
448
|
+
// on an answer that will never come.
|
|
449
|
+
if (this.autoCancelPendingToolCalls) {
|
|
450
|
+
for (const tc of toolCalls.values()) {
|
|
451
|
+
if (!registry.isRegistered(tc.name))
|
|
452
|
+
continue;
|
|
453
|
+
if (tc.result !== undefined || tc.followedUp)
|
|
454
|
+
continue;
|
|
455
|
+
tc.result = { error: "Tool call did not complete before the run ended." };
|
|
456
|
+
tc.isError = true;
|
|
457
|
+
yield {
|
|
458
|
+
type: "tool-call-result",
|
|
459
|
+
id: tc.id,
|
|
460
|
+
result: tc.result,
|
|
461
|
+
isError: true,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
393
465
|
// Decide whether to re-issue: any client-side tool that resolved
|
|
394
466
|
// during this turn and hasn't been replayed yet.
|
|
395
467
|
const pendingClientTools = Array.from(toolCalls.values()).filter((tc) => registry.isRegistered(tc.name) && tc.result !== undefined && !tc.followedUp);
|
|
@@ -71,6 +71,50 @@ export type RunnerEvent = {
|
|
|
71
71
|
} | {
|
|
72
72
|
type: "step-finished";
|
|
73
73
|
name: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Agent state published by the server (`STATE_SNAPSHOT` / `STATE_DELTA`).
|
|
77
|
+
* The AG-UI client applies deltas (JSON Patch) to its own state; this
|
|
78
|
+
* event carries the RESULT so consumers can react without reaching into
|
|
79
|
+
* the client. `patch` is present only for deltas, for consumers that want
|
|
80
|
+
* to know what changed rather than just the new value.
|
|
81
|
+
*/
|
|
82
|
+
| {
|
|
83
|
+
type: "agent-state";
|
|
84
|
+
state: unknown;
|
|
85
|
+
patch?: unknown[];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Server-authoritative history replacement (`MESSAGES_SNAPSHOT`). Emitted
|
|
89
|
+
* when the backend rewrites the conversation — e.g. after compaction. The
|
|
90
|
+
* runner does NOT interrupt an in-flight message for this: upstream saw
|
|
91
|
+
* mid-run snapshots truncate streaming answers, so consumers should
|
|
92
|
+
* reconcile persisted history and leave the streaming turn alone.
|
|
93
|
+
*/
|
|
94
|
+
| {
|
|
95
|
+
type: "messages-snapshot";
|
|
96
|
+
messages: unknown[];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Generative-UI surface (`ACTIVITY_SNAPSHOT` / `ACTIVITY_DELTA`) — MCP
|
|
100
|
+
* apps and A2UI. Passed through verbatim; hosts that don't render
|
|
101
|
+
* activities can ignore it.
|
|
102
|
+
*/
|
|
103
|
+
| {
|
|
104
|
+
type: "activity";
|
|
105
|
+
messageId: string;
|
|
106
|
+
activityType: string;
|
|
107
|
+
content?: unknown;
|
|
108
|
+
patch?: unknown[];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Provider passthrough (`RAW`). Never interpreted here — hosts use it for
|
|
112
|
+
* provider-specific behaviour and debugging.
|
|
113
|
+
*/
|
|
114
|
+
| {
|
|
115
|
+
type: "raw";
|
|
116
|
+
event: unknown;
|
|
117
|
+
source?: string;
|
|
74
118
|
} | {
|
|
75
119
|
type: "run-finished";
|
|
76
120
|
} | {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ export { AGUIRunner, type AGUIRunnerOptions, type AGUIRunInput } from "./client/
|
|
|
2
2
|
export type { RunnerEvent } from "./client/runner-events.js";
|
|
3
3
|
export { createServiceFetch, type ServiceFetch, type ServiceFetchOptions, } from "./client/service-fetch.js";
|
|
4
4
|
export { clientToolRegistry, type ClientToolEntry, type ToolRegistry } from "./tools/registry.js";
|
|
5
|
-
export {
|
|
5
|
+
export { agentStateStore } from "./store/agent-state-store.js";
|
|
6
|
+
export { reasoningDefaultStore, reasoningEffortStore, REASONING_EFFORT_LABELS, type ReasoningEffort, type ResolvedReasoningEffort, } from "./store/reasoning-effort-store.js";
|
|
6
7
|
export { streamingStatusStore, type StreamingStatus } from "./store/streaming-status-store.js";
|
|
7
8
|
export { citationStore, type CitationResult, type CitationHandler, } from "./store/citation-store.js";
|
|
8
9
|
export { linkStore, resolveLinkHandler, type LinkHandler, type ResolvedLinkHandler, } from "./store/link-store.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@ export { createServiceFetch, } from "./client/service-fetch.js";
|
|
|
5
5
|
// --- Tool registry ---
|
|
6
6
|
export { clientToolRegistry } from "./tools/registry.js";
|
|
7
7
|
// --- Stores (vanilla) ---
|
|
8
|
-
export {
|
|
8
|
+
export { agentStateStore } from "./store/agent-state-store.js";
|
|
9
|
+
export { reasoningDefaultStore, reasoningEffortStore, REASONING_EFFORT_LABELS, } from "./store/reasoning-effort-store.js";
|
|
9
10
|
export { streamingStatusStore } from "./store/streaming-status-store.js";
|
|
10
11
|
export { citationStore, } from "./store/citation-store.js";
|
|
11
12
|
export { linkStore, resolveLinkHandler, } from "./store/link-store.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface AgentStateState {
|
|
2
|
+
/** Latest server-published agent state, or `null` before the first snapshot. */
|
|
3
|
+
state: unknown;
|
|
4
|
+
/** JSON Patch from the most recent `STATE_DELTA`, if the last update was one. */
|
|
5
|
+
lastPatch: unknown[] | null;
|
|
6
|
+
setAgentState: (state: unknown, patch?: unknown[]) => void;
|
|
7
|
+
reset: () => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Server-published agent state (AG-UI `STATE_SNAPSHOT` / `STATE_DELTA`).
|
|
11
|
+
*
|
|
12
|
+
* The AG-UI client already applies snapshots and JSON-Patch deltas to its
|
|
13
|
+
* own internal state, but nothing surfaced that to the UI — a page could
|
|
14
|
+
* only read it by reaching into the transport. This store is the seam:
|
|
15
|
+
* the runner publishes each update, React consumers bind with
|
|
16
|
+
* `useStore(agentStateStore, selector)`.
|
|
17
|
+
*
|
|
18
|
+
* Vanilla store — this package stays zero-React.
|
|
19
|
+
*/
|
|
20
|
+
export declare const agentStateStore: import("zustand/vanilla").StoreApi<AgentStateState>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createStore } from "zustand/vanilla";
|
|
2
|
+
/**
|
|
3
|
+
* Server-published agent state (AG-UI `STATE_SNAPSHOT` / `STATE_DELTA`).
|
|
4
|
+
*
|
|
5
|
+
* The AG-UI client already applies snapshots and JSON-Patch deltas to its
|
|
6
|
+
* own internal state, but nothing surfaced that to the UI — a page could
|
|
7
|
+
* only read it by reaching into the transport. This store is the seam:
|
|
8
|
+
* the runner publishes each update, React consumers bind with
|
|
9
|
+
* `useStore(agentStateStore, selector)`.
|
|
10
|
+
*
|
|
11
|
+
* Vanilla store — this package stays zero-React.
|
|
12
|
+
*/
|
|
13
|
+
export const agentStateStore = createStore((set) => ({
|
|
14
|
+
state: null,
|
|
15
|
+
lastPatch: null,
|
|
16
|
+
setAgentState: (state, patch) => set({ state, lastPatch: patch ?? null }),
|
|
17
|
+
reset: () => set({ state: null, lastPatch: null }),
|
|
18
|
+
}));
|
|
@@ -1,12 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* How hard the model should think before answering.
|
|
3
3
|
*
|
|
4
|
-
* `"default"` sends nothing
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* `"default"` is a deferral, not a level: it sends nothing, so whatever the
|
|
5
|
+
* backend is configured for applies. Every other member maps to a real
|
|
6
|
+
* AG-UI/Responses reasoning effort and is sent verbatim.
|
|
7
|
+
*
|
|
8
|
+
* A deferral is only honest if the user can see what it currently resolves
|
|
9
|
+
* to — otherwise you believe you are on Extra High and are quietly running
|
|
10
|
+
* Instant, and the control silently decides cost and latency. That is what
|
|
11
|
+
* {@link reasoningDefaultStore} is for.
|
|
12
|
+
*/
|
|
13
|
+
export type ReasoningEffort = "default" | "low" | "medium" | "high" | "xhigh";
|
|
14
|
+
/** A real effort level — everything except the deferral. */
|
|
15
|
+
export type ResolvedReasoningEffort = Exclude<ReasoningEffort, "default">;
|
|
16
|
+
/**
|
|
17
|
+
* Level names follow the convention users already know from other assistants
|
|
18
|
+
* (Instant / Medium / High / Extra High) rather than inventing a private
|
|
19
|
+
* vocabulary. `Auto` is ours, for the case the protocol enum has no member
|
|
20
|
+
* for: follow whatever the app is configured for.
|
|
7
21
|
*/
|
|
8
|
-
export type ReasoningEffort = "default" | "low" | "medium" | "high";
|
|
9
22
|
export declare const REASONING_EFFORT_LABELS: Record<ReasoningEffort, string>;
|
|
23
|
+
interface ReasoningDefaultState {
|
|
24
|
+
/**
|
|
25
|
+
* What `"default"` resolves to right now, or `null` while unknown.
|
|
26
|
+
*
|
|
27
|
+
* `null` also covers a backend with reasoning switched off entirely, where
|
|
28
|
+
* there is no level to name.
|
|
29
|
+
*/
|
|
30
|
+
resolved: ResolvedReasoningEffort | null;
|
|
31
|
+
/**
|
|
32
|
+
* Where that came from, for the picker's hint — e.g. `"this workspace"`.
|
|
33
|
+
* `null` means unattributed, and the hint stays generic.
|
|
34
|
+
*/
|
|
35
|
+
scope: string | null;
|
|
36
|
+
setResolved: (resolved: ResolvedReasoningEffort | null, scope?: string | null) => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What the backend will actually do when the client sends no effort.
|
|
40
|
+
*
|
|
41
|
+
* Populated by the host app, never derived here. Re-deriving the backend's
|
|
42
|
+
* fallback chain client-side is exactly how a label goes stale and starts
|
|
43
|
+
* lying: the backend changes its default and the UI keeps claiming the old
|
|
44
|
+
* one. The app fetches this from the server and sets it, and updates it
|
|
45
|
+
* when the context it depends on changes — switching workspace, say — so
|
|
46
|
+
* `Auto` always names the level that will really be used.
|
|
47
|
+
*
|
|
48
|
+
* Left unset, the picker says it does not know rather than guessing.
|
|
49
|
+
*
|
|
50
|
+
* Vanilla store — this package stays zero-React.
|
|
51
|
+
*/
|
|
52
|
+
export declare const reasoningDefaultStore: import("zustand/vanilla").StoreApi<ReasoningDefaultState>;
|
|
10
53
|
interface ReasoningEffortState {
|
|
11
54
|
effort: ReasoningEffort;
|
|
12
55
|
setEffort: (effort: ReasoningEffort) => void;
|
|
@@ -17,6 +60,9 @@ interface ReasoningEffortState {
|
|
|
17
60
|
* Vanilla store (this package is zero-React); the runner reads it when
|
|
18
61
|
* building each request so the choice applies per turn — change it
|
|
19
62
|
* mid-conversation and the next message uses the new level.
|
|
63
|
+
*
|
|
64
|
+
* An explicit level is a pin: nothing moves it, including a change of
|
|
65
|
+
* context. Only `"default"` follows.
|
|
20
66
|
*/
|
|
21
67
|
export declare const reasoningEffortStore: import("zustand/vanilla").StoreApi<ReasoningEffortState>;
|
|
22
68
|
export {};
|
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
import { createStore } from "zustand/vanilla";
|
|
2
|
+
/**
|
|
3
|
+
* Level names follow the convention users already know from other assistants
|
|
4
|
+
* (Instant / Medium / High / Extra High) rather than inventing a private
|
|
5
|
+
* vocabulary. `Auto` is ours, for the case the protocol enum has no member
|
|
6
|
+
* for: follow whatever the app is configured for.
|
|
7
|
+
*/
|
|
2
8
|
export const REASONING_EFFORT_LABELS = {
|
|
3
9
|
default: "Auto",
|
|
4
|
-
low: "
|
|
5
|
-
medium: "
|
|
6
|
-
high: "
|
|
10
|
+
low: "Instant",
|
|
11
|
+
medium: "Medium",
|
|
12
|
+
high: "High",
|
|
13
|
+
xhigh: "Extra High",
|
|
7
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* What the backend will actually do when the client sends no effort.
|
|
17
|
+
*
|
|
18
|
+
* Populated by the host app, never derived here. Re-deriving the backend's
|
|
19
|
+
* fallback chain client-side is exactly how a label goes stale and starts
|
|
20
|
+
* lying: the backend changes its default and the UI keeps claiming the old
|
|
21
|
+
* one. The app fetches this from the server and sets it, and updates it
|
|
22
|
+
* when the context it depends on changes — switching workspace, say — so
|
|
23
|
+
* `Auto` always names the level that will really be used.
|
|
24
|
+
*
|
|
25
|
+
* Left unset, the picker says it does not know rather than guessing.
|
|
26
|
+
*
|
|
27
|
+
* Vanilla store — this package stays zero-React.
|
|
28
|
+
*/
|
|
29
|
+
export const reasoningDefaultStore = createStore((set) => ({
|
|
30
|
+
resolved: null,
|
|
31
|
+
scope: null,
|
|
32
|
+
setResolved: (resolved, scope) => set({ resolved, scope: scope ?? null }),
|
|
33
|
+
}));
|
|
8
34
|
const STORAGE_KEY = "foundry:reasoning-effort";
|
|
9
35
|
function readPersisted() {
|
|
10
36
|
if (typeof localStorage === "undefined")
|
|
11
37
|
return "default";
|
|
12
38
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
13
|
-
return raw === "low" || raw === "medium" || raw === "high" || raw === "
|
|
39
|
+
return raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "default"
|
|
40
|
+
? raw
|
|
41
|
+
: "default";
|
|
14
42
|
}
|
|
15
43
|
/**
|
|
16
44
|
* The user's chosen reasoning effort, persisted across reloads.
|
|
@@ -18,6 +46,9 @@ function readPersisted() {
|
|
|
18
46
|
* Vanilla store (this package is zero-React); the runner reads it when
|
|
19
47
|
* building each request so the choice applies per turn — change it
|
|
20
48
|
* mid-conversation and the next message uses the new level.
|
|
49
|
+
*
|
|
50
|
+
* An explicit level is a pin: nothing moves it, including a change of
|
|
51
|
+
* context. Only `"default"` follows.
|
|
21
52
|
*/
|
|
22
53
|
export const reasoningEffortStore = createStore((set) => ({
|
|
23
54
|
effort: readPersisted(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Cross-runtime AG-UI transport for Foundry UI — AGUIRunner protocol engine, vanilla zustand stores, optional MSAL auth subpath, service-fetch factory. Zero React, zero DOM.",
|
|
6
6
|
"keywords": [
|