@iloveagents/foundry-agent 0.8.0 → 0.9.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.
@@ -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;
@@ -244,6 +245,56 @@ export class AGUIRunner {
244
245
  onStepFinishedEvent: ({ event }) => {
245
246
  push({ type: "step-finished", name: event.stepName });
246
247
  },
248
+ // Reasoning has two nested boundaries: a BLOCK
249
+ // (REASONING_START…REASONING_END) may contain several MESSAGES
250
+ // (REASONING_MESSAGE_START…REASONING_MESSAGE_END). `reasoning-end`
251
+ // means "the model stopped thinking", so it is emitted only for the
252
+ // block terminal in `onReasoningEndEvent`. Emitting it per message
253
+ // too would fire it repeatedly and let consumers treat reasoning as
254
+ // finished while it is still going.
255
+ onReasoningStartEvent: () => {
256
+ push({ type: "streaming-status", status: { status: "reasoning" } });
257
+ },
258
+ // Server-published agent state. The AG-UI client has already
259
+ // applied the snapshot/patch to `httpAgent.state`; forward the
260
+ // result so consumers don't reach into the client.
261
+ onStateSnapshotEvent: () => {
262
+ push({ type: "agent-state", state: this.httpAgent.state });
263
+ },
264
+ onStateDeltaEvent: ({ event }) => {
265
+ push({
266
+ type: "agent-state",
267
+ state: this.httpAgent.state,
268
+ patch: event.delta,
269
+ });
270
+ },
271
+ // Server-authoritative history (e.g. after backend compaction).
272
+ // Deliberately does NOT touch the in-flight turn: upstream saw
273
+ // mid-run snapshots truncate a streaming answer.
274
+ onMessagesSnapshotEvent: ({ event }) => {
275
+ push({ type: "messages-snapshot", messages: event.messages });
276
+ },
277
+ // Generative-UI surfaces (MCP apps / A2UI), passed through.
278
+ onActivitySnapshotEvent: ({ event }) => {
279
+ push({
280
+ type: "activity",
281
+ messageId: event.messageId,
282
+ activityType: event.activityType,
283
+ content: event.content,
284
+ });
285
+ },
286
+ onActivityDeltaEvent: ({ event }) => {
287
+ push({
288
+ type: "activity",
289
+ messageId: event.messageId,
290
+ activityType: event.activityType,
291
+ patch: event.patch,
292
+ });
293
+ },
294
+ // Provider passthrough — never interpreted here.
295
+ onRawEvent: ({ event }) => {
296
+ push({ type: "raw", event: event.event, source: event.source });
297
+ },
247
298
  onCustomEvent: ({ event }) => {
248
299
  // Server heartbeat: liveness proof during long tool calls /
249
300
  // thinking phases (also keeps intermediary idle-timeouts at bay
@@ -390,6 +441,26 @@ export class AGUIRunner {
390
441
  abortSignal?.removeEventListener("abort", onAbort);
391
442
  await runPromise; // ensure the run task is settled
392
443
  }
444
+ // Any client-side tool that was STARTED but never produced a
445
+ // result leaves a `function_call` with no output. Settle it here so
446
+ // the replayed history stays valid and the model isn't left waiting
447
+ // on an answer that will never come.
448
+ if (this.autoCancelPendingToolCalls) {
449
+ for (const tc of toolCalls.values()) {
450
+ if (!registry.isRegistered(tc.name))
451
+ continue;
452
+ if (tc.result !== undefined || tc.followedUp)
453
+ continue;
454
+ tc.result = { error: "Tool call did not complete before the run ended." };
455
+ tc.isError = true;
456
+ yield {
457
+ type: "tool-call-result",
458
+ id: tc.id,
459
+ result: tc.result,
460
+ isError: true,
461
+ };
462
+ }
463
+ }
393
464
  // Decide whether to re-issue: any client-side tool that resolved
394
465
  // during this turn and hasn't been replayed yet.
395
466
  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,6 +2,7 @@ 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 { agentStateStore } from "./store/agent-state-store.js";
5
6
  export { reasoningEffortStore, REASONING_EFFORT_LABELS, type ReasoningEffort, } 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";
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ 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 { agentStateStore } from "./store/agent-state-store.js";
8
9
  export { 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";
@@ -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
+ }));
@@ -5,7 +5,13 @@
5
5
  * alone — the user hasn't expressed a preference, so we don't override
6
6
  * one. The remaining levels map to the AG-UI/Responses reasoning effort.
7
7
  */
8
- export type ReasoningEffort = "default" | "low" | "medium" | "high";
8
+ export type ReasoningEffort = "default" | "low" | "medium" | "high" | "xhigh";
9
+ /**
10
+ * Level names follow the convention users already know from other assistants
11
+ * (Instant / Medium / High / Extra High) rather than inventing a private
12
+ * vocabulary. `Auto` is ours: it means "no preference — use whatever the app
13
+ * is configured for", which the protocol enum has no member for.
14
+ */
9
15
  export declare const REASONING_EFFORT_LABELS: Record<ReasoningEffort, string>;
10
16
  interface ReasoningEffortState {
11
17
  effort: ReasoningEffort;
@@ -1,16 +1,25 @@
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: it means "no preference — use whatever the app
6
+ * is configured for", which the protocol enum has no member for.
7
+ */
2
8
  export const REASONING_EFFORT_LABELS = {
3
9
  default: "Auto",
4
- low: "Fast",
5
- medium: "Balanced",
6
- high: "Thorough",
10
+ low: "Instant",
11
+ medium: "Medium",
12
+ high: "High",
13
+ xhigh: "Extra High",
7
14
  };
8
15
  const STORAGE_KEY = "foundry:reasoning-effort";
9
16
  function readPersisted() {
10
17
  if (typeof localStorage === "undefined")
11
18
  return "default";
12
19
  const raw = localStorage.getItem(STORAGE_KEY);
13
- return raw === "low" || raw === "medium" || raw === "high" || raw === "default" ? raw : "default";
20
+ return raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "default"
21
+ ? raw
22
+ : "default";
14
23
  }
15
24
  /**
16
25
  * The user's chosen reasoning effort, persisted across reloads.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-agent",
3
- "version": "0.8.0",
3
+ "version": "0.9.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": [