@iloveagents/foundry-agent 0.7.1 → 0.8.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.
@@ -14,6 +14,7 @@
14
14
  * @see https://docs.ag-ui.com/sdk/js/client/subscriber
15
15
  */
16
16
  import { HttpAgent } from "@ag-ui/client";
17
+ import { reasoningEffortStore } from "../store/reasoning-effort-store.js";
17
18
  function shouldPreserveAcrossVisibleHistory(message) {
18
19
  return message.role === "system" || message.role === "developer" || message.role === "reasoning";
19
20
  }
@@ -147,6 +148,12 @@ export class AGUIRunner {
147
148
  // Build the request input now, before runAgent fires — dev tooling
148
149
  // consumes this snapshot via the request-sent event. `runId` is
149
150
  // pre-generated so the snapshot matches what runAgent emits.
151
+ // The user's reasoning-effort choice rides on every turn as an
152
+ // AG-UI forwardedProp — read at request time so changing it
153
+ // mid-conversation takes effect on the next message. "default"
154
+ // sends nothing so the backend keeps its configured level.
155
+ const chosenEffort = reasoningEffortStore.getState().effort;
156
+ const forwardedProps = chosenEffort === "default" ? {} : { reasoningEffort: chosenEffort };
150
157
  const runInputSnapshot = {
151
158
  threadId: this.httpAgent.threadId,
152
159
  runId,
@@ -154,6 +161,7 @@ export class AGUIRunner {
154
161
  messages: currentMessages,
155
162
  tools,
156
163
  context: context ?? [],
164
+ forwardedProps,
157
165
  };
158
166
  // --- Liveness + protocol-integrity bookkeeping (per turn) ---
159
167
  // AG-UI requires a terminal RUN_FINISHED or RUN_ERROR. A stream that
@@ -211,6 +219,31 @@ export class AGUIRunner {
211
219
  onTextMessageEndEvent: () => {
212
220
  push({ type: "text-message-end" });
213
221
  },
222
+ // Reasoning/thinking. `@ag-ui/client` normalizes the deprecated
223
+ // THINKING_* events onto REASONING_* via its backward-compat
224
+ // middleware, so subscribing here covers both wire dialects.
225
+ onReasoningMessageStartEvent: () => {
226
+ push({ type: "streaming-status", status: { status: "reasoning" } });
227
+ },
228
+ onReasoningMessageContentEvent: ({ event }) => {
229
+ push({ type: "reasoning-delta", delta: event.delta });
230
+ },
231
+ onReasoningEndEvent: () => {
232
+ push({ type: "reasoning-end" });
233
+ // Reasoning ended but the answer hasn't started — back to the
234
+ // generic working state rather than leaving "reasoning" stuck.
235
+ push({ type: "streaming-status", status: { status: "thinking" } });
236
+ },
237
+ onStepStartedEvent: ({ event }) => {
238
+ push({ type: "step-started", name: event.stepName });
239
+ push({
240
+ type: "streaming-status",
241
+ status: { status: lastStatus.status, stepName: event.stepName },
242
+ });
243
+ },
244
+ onStepFinishedEvent: ({ event }) => {
245
+ push({ type: "step-finished", name: event.stepName });
246
+ },
214
247
  onCustomEvent: ({ event }) => {
215
248
  // Server heartbeat: liveness proof during long tool calls /
216
249
  // thinking phases (also keeps intermediary idle-timeouts at bay
@@ -320,7 +353,7 @@ export class AGUIRunner {
320
353
  // the protocol violation here.
321
354
  // 2. Transport-level rejection (fetch failure, TLS reset).
322
355
  const runPromise = this.httpAgent
323
- .runAgent({ runId, tools, context: context ?? [] }, subscriber)
356
+ .runAgent({ runId, tools, context: context ?? [], forwardedProps }, subscriber)
324
357
  .then(() => {
325
358
  if (!sawTerminal && !abortSignal?.aborted) {
326
359
  push({ type: "streaming-status", status: { status: "idle" } });
@@ -366,29 +399,59 @@ export class AGUIRunner {
366
399
  tc.followedUp = true;
367
400
  // Build follow-up messages with the SAME toolCallId the agent emitted.
368
401
  // Tool results are appended to the agent's message history (AG-UI
369
- // convention — append, never replace). Preserve any pre-tool text
370
- // the model emitted in this turn so the next-turn context matches
371
- // what the user actually saw.
372
- const assistantMsg = {
373
- id: crypto.randomUUID(),
374
- role: "assistant",
375
- content: turnAssistantText,
376
- toolCalls: pendingClientTools.map((tc) => ({
377
- id: tc.id,
378
- type: "function",
379
- function: { name: tc.name, arguments: tc.args },
380
- })),
381
- };
382
- const toolResultMsgs = pendingClientTools.map((tc) => ({
383
- id: crypto.randomUUID(),
384
- role: "tool",
385
- toolCallId: tc.id,
386
- content: typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result),
387
- }));
388
- currentMessages = [...currentMessages, assistantMsg, ...toolResultMsgs];
389
- for (const m of [assistantMsg, ...toolResultMsgs]) {
402
+ // convention — append, never replace).
403
+ //
404
+ // The AG-UI client folds the streamed assistant message — tool calls
405
+ // included into `httpAgent.messages` itself (AbstractAgent applies
406
+ // the event stream to its own history). Appending an unconditional
407
+ // second copy put the SAME toolCallId in the history twice with only
408
+ // one matching tool result, so the model saw an unanswered tool call:
409
+ // it re-issued the tool (a duplicate card in the UI) and then reported
410
+ // that the call "didn't complete", even though the tool had succeeded.
411
+ // Only synthesize what the client hasn't already recorded.
412
+ const recordedToolCallIds = new Set();
413
+ const recordedToolResultIds = new Set();
414
+ for (const message of this.httpAgent.messages) {
415
+ if (message.role === "assistant" && message.toolCalls) {
416
+ for (const call of message.toolCalls)
417
+ recordedToolCallIds.add(call.id);
418
+ }
419
+ else if (message.role === "tool" && message.toolCallId) {
420
+ recordedToolResultIds.add(message.toolCallId);
421
+ }
422
+ }
423
+ const followUpMessages = [];
424
+ // Preserve any pre-tool text the model emitted in this turn so the
425
+ // next-turn context matches what the user actually saw.
426
+ const unrecordedToolCalls = pendingClientTools.filter((tc) => !recordedToolCallIds.has(tc.id));
427
+ if (unrecordedToolCalls.length > 0) {
428
+ followUpMessages.push({
429
+ id: crypto.randomUUID(),
430
+ role: "assistant",
431
+ content: turnAssistantText,
432
+ toolCalls: unrecordedToolCalls.map((tc) => ({
433
+ id: tc.id,
434
+ type: "function",
435
+ function: { name: tc.name, arguments: tc.args },
436
+ })),
437
+ });
438
+ }
439
+ for (const tc of pendingClientTools) {
440
+ if (recordedToolResultIds.has(tc.id))
441
+ continue;
442
+ followUpMessages.push({
443
+ id: crypto.randomUUID(),
444
+ role: "tool",
445
+ toolCallId: tc.id,
446
+ content: typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result),
447
+ });
448
+ }
449
+ for (const m of followUpMessages) {
390
450
  this.httpAgent.addMessage(m);
391
451
  }
452
+ // Re-sync from the client's history: it is the authority on what the
453
+ // next request carries (`prepareRunAgentInput` reads it directly).
454
+ currentMessages = [...this.httpAgent.messages];
392
455
  }
393
456
  }
394
457
  finally {
@@ -46,6 +46,31 @@ export type RunnerEvent = {
46
46
  } | {
47
47
  type: "streaming-status";
48
48
  status: StreamingStatus;
49
+ }
50
+ /**
51
+ * Model reasoning/thinking text (AG-UI `REASONING_*`; the deprecated
52
+ * `THINKING_*` events are normalized onto these by `@ag-ui/client`'s
53
+ * backward-compatibility middleware). Kept separate from `text-delta`
54
+ * because reasoning is not the answer — consumers render it as its own
55
+ * collapsed part, never inline with the reply.
56
+ */
57
+ | {
58
+ type: "reasoning-delta";
59
+ delta: string;
60
+ } | {
61
+ type: "reasoning-end";
62
+ }
63
+ /**
64
+ * Named agent step (AG-UI `STEP_STARTED` / `STEP_FINISHED`). A long
65
+ * agentic run otherwise presents as one opaque "thinking" state for
66
+ * minutes; the step name is what makes the wait legible.
67
+ */
68
+ | {
69
+ type: "step-started";
70
+ name: string;
71
+ } | {
72
+ type: "step-finished";
73
+ name: string;
49
74
  } | {
50
75
  type: "run-finished";
51
76
  } | {
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 { reasoningEffortStore, REASONING_EFFORT_LABELS, type ReasoningEffort, } from "./store/reasoning-effort-store.js";
5
6
  export { streamingStatusStore, type StreamingStatus } from "./store/streaming-status-store.js";
6
7
  export { citationStore, type CitationResult, type CitationHandler, } from "./store/citation-store.js";
7
8
  export { linkStore, resolveLinkHandler, type LinkHandler, type ResolvedLinkHandler, } from "./store/link-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 { reasoningEffortStore, REASONING_EFFORT_LABELS, } from "./store/reasoning-effort-store.js";
8
9
  export { streamingStatusStore } from "./store/streaming-status-store.js";
9
10
  export { citationStore, } from "./store/citation-store.js";
10
11
  export { linkStore, resolveLinkHandler, } from "./store/link-store.js";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * How hard the model should think before answering.
3
+ *
4
+ * `"default"` sends nothing and leaves the backend's configured level
5
+ * alone — the user hasn't expressed a preference, so we don't override
6
+ * one. The remaining levels map to the AG-UI/Responses reasoning effort.
7
+ */
8
+ export type ReasoningEffort = "default" | "low" | "medium" | "high";
9
+ export declare const REASONING_EFFORT_LABELS: Record<ReasoningEffort, string>;
10
+ interface ReasoningEffortState {
11
+ effort: ReasoningEffort;
12
+ setEffort: (effort: ReasoningEffort) => void;
13
+ }
14
+ /**
15
+ * The user's chosen reasoning effort, persisted across reloads.
16
+ *
17
+ * Vanilla store (this package is zero-React); the runner reads it when
18
+ * building each request so the choice applies per turn — change it
19
+ * mid-conversation and the next message uses the new level.
20
+ */
21
+ export declare const reasoningEffortStore: import("zustand/vanilla").StoreApi<ReasoningEffortState>;
22
+ export {};
@@ -0,0 +1,30 @@
1
+ import { createStore } from "zustand/vanilla";
2
+ export const REASONING_EFFORT_LABELS = {
3
+ default: "Auto",
4
+ low: "Fast",
5
+ medium: "Balanced",
6
+ high: "Thorough",
7
+ };
8
+ const STORAGE_KEY = "foundry:reasoning-effort";
9
+ function readPersisted() {
10
+ if (typeof localStorage === "undefined")
11
+ return "default";
12
+ const raw = localStorage.getItem(STORAGE_KEY);
13
+ return raw === "low" || raw === "medium" || raw === "high" || raw === "default" ? raw : "default";
14
+ }
15
+ /**
16
+ * The user's chosen reasoning effort, persisted across reloads.
17
+ *
18
+ * Vanilla store (this package is zero-React); the runner reads it when
19
+ * building each request so the choice applies per turn — change it
20
+ * mid-conversation and the next message uses the new level.
21
+ */
22
+ export const reasoningEffortStore = createStore((set) => ({
23
+ effort: readPersisted(),
24
+ setEffort: (effort) => {
25
+ if (typeof localStorage !== "undefined") {
26
+ localStorage.setItem(STORAGE_KEY, effort);
27
+ }
28
+ set({ effort });
29
+ },
30
+ }));
@@ -1,7 +1,14 @@
1
1
  /** Streaming status emitted by the AG-UI runner. */
2
2
  export interface StreamingStatus {
3
- status: "thinking" | "calling" | "streaming" | "stalled" | "idle";
3
+ status: "thinking" | "reasoning" | "calling" | "streaming" | "stalled" | "idle";
4
4
  toolName?: string;
5
+ /**
6
+ * Name of the agent step currently running, from AG-UI `STEP_STARTED`.
7
+ * Long agentic runs otherwise show one opaque "thinking" state for
8
+ * minutes; the step name is what makes that wait legible. Undefined when
9
+ * the agent doesn't emit step events.
10
+ */
11
+ stepName?: string;
5
12
  }
6
13
  interface StreamingStatusState {
7
14
  streamingStatus: StreamingStatus;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-agent",
3
- "version": "0.7.1",
3
+ "version": "0.8.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": [