@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.
- package/dist/client/agui-runner.js +85 -22
- package/dist/client/runner-events.d.ts +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/store/reasoning-effort-store.d.ts +22 -0
- package/dist/store/reasoning-effort-store.js +30 -0
- package/dist/store/streaming-status-store.d.ts +8 -1
- package/package.json +1 -1
|
@@ -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).
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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.
|
|
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": [
|