@iloveagents/foundry-agent 0.3.0 → 0.4.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.
Files changed (51) hide show
  1. package/README.md +16 -0
  2. package/dist/client/agui-runner.d.ts +53 -0
  3. package/dist/client/agui-runner.js +320 -0
  4. package/dist/client/runner-events.d.ts +54 -0
  5. package/dist/client/runner-events.js +1 -0
  6. package/dist/client/service-fetch.d.ts +112 -0
  7. package/dist/client/service-fetch.js +244 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +10 -0
  10. package/dist/msal/auth-config.d.ts +91 -0
  11. package/dist/msal/auth-config.js +70 -0
  12. package/dist/msal/auth-store.d.ts +95 -0
  13. package/dist/msal/auth-store.js +372 -0
  14. package/dist/msal/index.d.ts +3 -0
  15. package/dist/msal/index.js +3 -0
  16. package/dist/msal/token-fetch.d.ts +16 -0
  17. package/dist/msal/token-fetch.js +57 -0
  18. package/dist/store/citation-store.d.ts +42 -0
  19. package/dist/store/citation-store.js +14 -0
  20. package/dist/store/link-store.d.ts +29 -0
  21. package/dist/store/link-store.js +28 -0
  22. package/dist/store/streaming-status-store.d.ts +15 -0
  23. package/dist/store/streaming-status-store.js +9 -0
  24. package/dist/tools/registry.d.ts +48 -0
  25. package/dist/tools/registry.js +50 -0
  26. package/package.json +23 -9
  27. package/AGENTS.md +0 -91
  28. package/CHANGELOG.md +0 -180
  29. package/CLAUDE.md +0 -1
  30. package/src/__tests__/agui-runner.test.ts +0 -404
  31. package/src/__tests__/auth-store.test.ts +0 -596
  32. package/src/__tests__/citation-store.test.ts +0 -52
  33. package/src/__tests__/client-tool-registry.test.ts +0 -84
  34. package/src/__tests__/link-store.test.ts +0 -48
  35. package/src/__tests__/service-fetch.test.ts +0 -525
  36. package/src/__tests__/streaming-status-store.test.ts +0 -22
  37. package/src/__tests__/token-fetch.test.ts +0 -134
  38. package/src/client/agui-runner.ts +0 -382
  39. package/src/client/runner-events.ts +0 -27
  40. package/src/client/service-fetch.ts +0 -318
  41. package/src/index.ts +0 -27
  42. package/src/msal/auth-config.ts +0 -150
  43. package/src/msal/auth-store.ts +0 -517
  44. package/src/msal/index.ts +0 -14
  45. package/src/msal/token-fetch.ts +0 -68
  46. package/src/store/citation-store.ts +0 -52
  47. package/src/store/link-store.ts +0 -53
  48. package/src/store/streaming-status-store.ts +0 -21
  49. package/src/tools/registry.ts +0 -112
  50. package/tsconfig.json +0 -15
  51. package/vitest.config.ts +0 -8
@@ -0,0 +1,50 @@
1
+ import { createStore } from "zustand/vanilla";
2
+ export const clientToolRegistry = createStore((set, get) => ({
3
+ globalTools: new Map(),
4
+ pageTools: new Map(),
5
+ registerGlobal: (tool) => {
6
+ const { globalTools } = get();
7
+ const next = new Map(globalTools);
8
+ next.set(tool.name, tool);
9
+ set({ globalTools: next });
10
+ },
11
+ registerPageTools: (tools) => {
12
+ const next = new Map();
13
+ for (const tool of tools) {
14
+ next.set(tool.name, tool);
15
+ }
16
+ set({ pageTools: next });
17
+ },
18
+ clearPageTools: () => {
19
+ set({ pageTools: new Map() });
20
+ },
21
+ getActiveSchemas: () => {
22
+ const { globalTools, pageTools } = get();
23
+ // Merge: page tools override global tools with the same name
24
+ const merged = new Map();
25
+ for (const tool of globalTools.values()) {
26
+ merged.set(tool.name, tool);
27
+ }
28
+ for (const tool of pageTools.values()) {
29
+ merged.set(tool.name, tool); // page overrides global
30
+ }
31
+ return Array.from(merged.values()).map((tool) => ({
32
+ name: tool.name,
33
+ description: tool.description,
34
+ parameters: tool.parameters,
35
+ }));
36
+ },
37
+ isRegistered: (name) => {
38
+ const { globalTools, pageTools } = get();
39
+ return pageTools.has(name) || globalTools.has(name);
40
+ },
41
+ executeTool: async (name, argsJson) => {
42
+ const { globalTools, pageTools } = get();
43
+ // Page tools take precedence
44
+ const tool = pageTools.get(name) ?? globalTools.get(name);
45
+ if (!tool) {
46
+ return JSON.stringify({ error: `Unknown client tool: ${name}` });
47
+ }
48
+ return tool.execute(argsJson);
49
+ },
50
+ }));
package/package.json CHANGED
@@ -1,13 +1,25 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-agent",
3
- "version": "0.3.0",
4
- "license": "SEE LICENSE IN LICENSE",
3
+ "version": "0.4.0",
4
+ "license": "MIT",
5
5
  "type": "module",
6
- "types": "./src/index.ts",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
7
8
  "exports": {
8
- ".": "./src/index.ts",
9
- "./msal": "./src/msal/index.ts"
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./msal": {
14
+ "types": "./dist/msal/index.d.ts",
15
+ "import": "./dist/msal/index.js"
16
+ }
10
17
  },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
11
23
  "publishConfig": {
12
24
  "access": "public"
13
25
  },
@@ -23,14 +35,16 @@
23
35
  }
24
36
  },
25
37
  "devDependencies": {
38
+ "@ag-ui/client": "^0.0.52",
39
+ "@ag-ui/core": "^0.0.52",
40
+ "@azure/msal-browser": "^5.0.0",
41
+ "jsdom": "^28.1.0",
26
42
  "typescript": "~5.9.3",
27
43
  "vitest": "^4.1.4",
28
- "jsdom": "^28.1.0",
29
- "zustand": "^5.0.0",
30
- "@ag-ui/client": "^0.0.52",
31
- "@ag-ui/core": "^0.0.52"
44
+ "zustand": "^5.0.0"
32
45
  },
33
46
  "scripts": {
47
+ "build": "tsc -p tsconfig.build.json && node ../../scripts/fix-dts-extensions.mjs dist",
34
48
  "test:unit": "vitest run",
35
49
  "typecheck": "tsc --noEmit"
36
50
  }
package/AGENTS.md DELETED
@@ -1,91 +0,0 @@
1
- Pure-TS agent transport for the LastSpace open-core stack. Published as `@iloveagents/foundry-agent`. Zero DOM, zero React, zero `@assistant-ui/*`. Cross-runtime — consumed by `@iloveagents/foundry-web-ui` today, future Outlook / Teams / native shells tomorrow.
2
-
3
- # Architecture
4
-
5
- ```
6
- src/
7
- client/
8
- agui-runner.ts ← AG-UI protocol engine (SSE + multi-turn loop). Yields RunnerEvent.
9
- runner-events.ts ← RunnerEvent discriminated union.
10
- service-fetch.ts ← createServiceFetch({ acquireToken, baseUrl }) factory.
11
- store/
12
- streaming-status-store.ts ← vanilla zustand store
13
- citation-store.ts ← vanilla zustand store
14
- tools/
15
- registry.ts ← clientToolRegistry (vanilla zustand store).
16
- msal/ ← subpath export: @iloveagents/foundry-agent/msal
17
- auth-store.ts ← authStore (vanilla zustand store)
18
- auth-config.ts ← MsalAuthConfig + initializeMsal singleton.
19
- token-fetch.ts ← Bearer-token-attaching fetch.
20
- index.ts ← barrel for the /msal subpath
21
- index.ts ← barrel for the root export.
22
- __tests__/ ← vitest suite (runner, service-fetch, no-react smoke).
23
- ```
24
-
25
- # Import boundary (enforced by guard test)
26
-
27
- Files under `packages/agent/src/` (excluding `msal/`) MUST NOT import:
28
-
29
- - `react`, `react-dom`
30
- - `@assistant-ui/*` (anywhere — including under `msal/`)
31
- - `@azure/msal-*` (anywhere — including under `msal/` for `msal-react`; only `@azure/msal-browser` is allowed under `msal/`)
32
- - bare `"zustand"` — use `"zustand/vanilla"` (`zustand/middleware` is allowed)
33
-
34
- Files under `packages/agent/src/msal/` MAY import `@azure/msal-browser` (dynamic import only — keeps the dep optional). They MUST NOT import `@azure/msal-react` (which is React-bound and lives in `@iloveagents/foundry-web-ui`).
35
-
36
- `packages/agent/` MUST NOT import from any other `@lastspace/*` package — agent is the leaf of the open-core graph.
37
-
38
- # Why `zustand/vanilla`
39
-
40
- `import { create } from "zustand"` resolves to the React build (`useSyncExternalStore`). Importing it in a "zero React" package would silently take a React dependency and defeat the cross-runtime goal. Every store in this package uses `import { createStore } from "zustand/vanilla"`. React consumers (in `@iloveagents/foundry-web-ui` / `apps/web/`) bind via `useStore(store, selector)` from the React entry of zustand.
41
-
42
- # What goes here
43
-
44
- - AG-UI protocol client (SSE, message conversion, multi-turn re-issue, tool dispatch).
45
- - Vanilla state stores that the protocol engine needs at runtime — streaming status, citation cache, client-tool registry.
46
- - MSAL bits under the `/msal` subpath only.
47
- - Generic per-service fetch factory (`createServiceFetch`).
48
-
49
- # What does NOT go here
50
-
51
- - Anything implementing `@assistant-ui/*` interfaces (`ChatModelAdapter`, `AttachmentAdapter`, etc.) — those are React UI surface and live in `@iloveagents/foundry-web-ui`.
52
- - React components, hooks, contexts, providers.
53
- - UI-layout state (panel widths, modal modes, theme).
54
- - The `MsalProvider` React component — that lives in `@iloveagents/foundry-web-ui`.
55
-
56
- # Public surface
57
-
58
- Root `@iloveagents/foundry-agent`:
59
-
60
- ```ts
61
- import {
62
- AGUIRunner,
63
- type RunnerEvent,
64
- createServiceFetch,
65
- clientToolRegistry,
66
- type ClientToolEntry,
67
- streamingStatusStore,
68
- type StreamingStatus,
69
- citationStore,
70
- type CitationResult,
71
- type CitationHandler,
72
- } from "@iloveagents/foundry-agent";
73
- ```
74
-
75
- Subpath `@iloveagents/foundry-agent/msal`:
76
-
77
- ```ts
78
- import {
79
- authStore,
80
- type AuthUser,
81
- type MsalAuthConfig,
82
- initializeMsal,
83
- getMsalInstance,
84
- getMsalConfig,
85
- tokenFetch,
86
- } from "@iloveagents/foundry-agent/msal";
87
- ```
88
-
89
- # Bundle-size budget
90
-
91
- Target ≤25 KB gz on the root entry (`@iloveagents/foundry-agent`, excluding the `/msal` subpath). Measured manually pre-1.0 via `gzip -c dist/index.js | wc -c`; `size-limit` enforcement scaffolds in #92's follow-up.
package/CHANGELOG.md DELETED
@@ -1,180 +0,0 @@
1
- # @iloveagents/foundry-agent
2
-
3
- ## 0.3.0
4
-
5
- ## 0.2.2
6
-
7
- ### Patch Changes
8
-
9
- - 8184a8c: shell: URL is authoritative for the runtime threadId — fixes
10
- resume-creates-new-conversation race
11
-
12
- `ChatConversationAwareRuntime` previously computed
13
- `effectiveThreadId = sticky ?? urlMatch ?? freshIdRef.current`.
14
- When the user navigated from one chat to another via a Recents
15
- click, the synchronous render that followed the URL change had:
16
- - `urlMatch = B` (read from the now-updated `useLocation`)
17
- - `sticky = A` (the active-chat-store hadn't been updated by
18
- `useTrackActiveChatFromUrl`'s `useEffect` yet — effects run
19
- AFTER the commit phase)
20
-
21
- `sticky ?? urlMatch` picked `A` (the previous chat). The AG-UI
22
- adapter was constructed with `threadId = A`. The first message the
23
- user sent went to `/api/agent` with `thread_id = A`, the middleware
24
- created a new row keyed by `A`... wait actually no, here's what
25
- happens — the runtime mints a fresh runner UUID when given a
26
- mismatched threadId; that fresh UUID became a new conversation row,
27
- appearing as "Untitled chat" at the top of the sidebar while the
28
- URL still said `/chat/B`. Two active-looking dots: one for `B`
29
- (NavLink URL match), one for the new row (statusDot from the
30
- updated active-chat-store after the lazy ensure ran).
31
-
32
- Swap the priority: `urlMatch ?? sticky ?? freshIdRef.current`. The
33
- URL is authoritative whenever it's set (i.e. on `/chat/<id>`
34
- routes), eliminating the stale-sticky race entirely. `sticky` is
35
- still consulted as the second-priority fallback for non-chat
36
- routes (`/spaces`, `/tasks`) so the popout chat stays "live" while
37
- the user browses workspaces — that's the original purpose of
38
- `useStickyConversationId` and it's preserved.
39
-
40
- ## 0.2.1
41
-
42
- ### Patch Changes
43
-
44
- - No source changes. The 0.2.0 publish for `@iloveagents/foundry-agent` and
45
- `@iloveagents/foundry-web-primitives` failed because npm had `0.2.0`
46
- reserved for those two packages from an older publish-then-unpublish
47
- cycle (the version was reserved before the recent v1 incident — see
48
- git tag history). `@iloveagents/foundry-web-shell@0.2.0` and
49
- `@iloveagents/foundry-web-ui@0.2.0` published cleanly. Bumping the
50
- whole fixed group to 0.2.1 so all four publish together at a version
51
- number that's free across all four packages.
52
-
53
- ## 0.2.0
54
-
55
- ### Minor Changes
56
-
57
- - shell: ChatConversationConfig API + sticky-runtime threadId
58
-
59
- Lets a downstream module (e.g. `@ltwlf/spaces-web-ui`) attach a
60
- `ChatConversationConfig` to its `ChatModule`. The shell then:
61
- - Pre-mints a fresh thread UUID via `useRef` so the AG-UI runtime's
62
- `threadId` is defined from first render — no `undefined → defined`
63
- remount mid-conversation when the user sends the first message.
64
- - Calls `config.useStickyConversationId()` (when declared) and
65
- feeds the resulting id into the runtime so the chat stays "live"
66
- while the user browses non-chat routes (Workspaces, Jobs, etc.).
67
- - Per-config keyed child components (`StickyAwareRuntime` vs
68
- `UrlOnlyRuntime`) so optional hooks satisfy Rules-of-Hooks across
69
- multi-module config swaps.
70
-
71
- UI: collapsible nav groups in `sidebar.tsx` with persisted state via
72
- `useSyncExternalStore` + localStorage, plus stable group ordering via
73
- new `priority` field in `nav-config.ts`.
74
-
75
- `ag-ui-runtime-provider.tsx` accepts an external `threadId` and
76
- `historyAdapterFactory` so the consuming app can wire its own
77
- persistence (e.g. `ThreadHistoryAdapter` → REST `/api/conversations`).
78
-
79
- ### Patch Changes
80
-
81
- - Fix `citationStore.clear()` so the registered handler survives per-conversation resets.
82
-
83
- The previous implementation wiped both `results` AND `handler`. `useNewConversation` calls `clear()` on every "New Thread" click. Combined with the `registerOnce`-style handler registration in feature modules (e.g. SPACES' `registerSpacesCitationHandler`), the result was that all `[n]` citation markers in chat became inert non-clickable spans for the rest of the session after the first new-thread click. `clear()` now resets `results` only — the handler is module-level state and must persist.
84
-
85
- - Move sibling `@iloveagents/foundry-*` declarations from `peerDependencies`
86
- to `dependencies` in `foundry-web-shell` and `foundry-web-ui`. These four
87
- packages always ship together as a coordinated fixed group from the same
88
- monorepo — they were never independently-versioned peers, so declaring
89
- them as peerDeps caused changesets' `shouldBumpMajor` cascade to promote
90
- the entire group to a major version on every minor changeset (a single
91
- minor on any of the four would exit the sibling's `^0.x.y` peerDep range
92
- → all dependents promoted to major → fixed group synced all four to
93
- major → 0.1.5 unexpectedly jumped to 1.0.0). Moving to `dependencies`
94
- eliminates the cascade entirely; `dependencies` never trigger
95
- `shouldBumpMajor`. Consumers that already install each foundry-\* package
96
- directly are unaffected — pnpm hoists by name, so a single instance
97
- resolves across the dep graph regardless of declaration site.
98
-
99
- ### NOTE: 1.0.0 / 1.0.1 were accidental
100
-
101
- These version numbers were published briefly on 2026-05-27 due to the
102
- peerDep cascade bug described above, then unpublished. The version
103
- numbers themselves are now permanently reserved on npm and CANNOT be
104
- re-published. This 0.2.0 release captures all the work that was in
105
- the accidental 1.0.x releases. Do not depend on 1.0.x.
106
-
107
- ## 0.1.5
108
-
109
- ## 0.1.4
110
-
111
- ### Patch Changes
112
-
113
- - 07ace7e: fix(agent): canonical MSAL.js recovery — acquireTokenRedirect-first, orphaned-state cleanup, dedup
114
-
115
- Closes the production "stuck on 401 forever" bug observed on andritz-dev after the 0.1.2 / 0.1.3 fixes. Diagnostic on the live tab showed `getAllAccounts() === []` despite localStorage holding 3 access tokens + 1 refresh token — the orphaned-state signature.
116
-
117
- **Root cause:** The previous recovery code did `setActiveAccount(null) + clearCache({account}) + loginRedirect()`. When `loginRedirect()` silently failed to navigate (caught by our try/catch — popup blocker, browser policy, async race), the account record was already gone but the tokens lingered. Every subsequent API call hit `getAllAccounts() === []` and silently returned `null` — no Bearer header, endless 401s, only manual `localStorage.clear()` recovered.
118
-
119
- **Canonical 2026 fix per Microsoft Learn `entra/msal/javascript/browser/errors`** + linked GitHub issues + msal-react samples:
120
- 1. **`acquireTokenRedirect` first, not `loginRedirect`.** It's the documented primitive for refreshing a known account's tokens — preserves the account record across the navigation. `loginRedirect` is only the fallback when `acquireTokenRedirect` itself fails to navigate.
121
- 2. **Never `clearCache` before redirect.** The redirect navigation IS the recovery signal; if it succeeds, MSAL handles state cleanup; if it fails, leave state intact for the next attempt rather than orphaning the cache.
122
- 3. **Detect + repair orphaned state.** When `getAllAccounts() === []` but localStorage has MSAL token entries, nuke ALL `msal.*` keys (localStorage + sessionStorage) and force a fresh `loginRedirect`. Without this, the SPA renders authenticated UI but every API call goes anonymous.
123
- 4. **Module-level recovery deduplication.** Multiple parallel API calls all hitting the recovery path simultaneously now share a single in-flight redirect promise instead of each issuing their own (which would cascade `interaction_in_progress` errors).
124
-
125
- The previous `recoverFromHardAuthFailure` callback now uses the same primitive — single recovery path for both "MSAL silent failed" and "API rejected refreshed token".
126
-
127
- 68/68 agent tests pass (was 65) — the new tests assert the no-clearCache invariant, the acquireTokenRedirect-first ordering, the loginRedirect fallback path, and the orphaned-state nuke + redirect. Pair: lastspace bump to ^0.1.4.
128
-
129
- ## 0.1.3
130
-
131
- ### Patch Changes
132
-
133
- - d9b0460: fix(agent): escalate to loginRedirect after second 401
134
-
135
- Follow-up to the auth-stale-token-401-retry fix in 0.1.2. The first version retried with `forceRefresh: true` and bounced to `loginRedirect` only when MSAL itself reported `InteractionRequiredAuthError`. But there's a more common production failure mode the silent path can't recover: the resource server rejects the **freshly-refreshed** token too — server-side policy drift, audience mismatch, conditional-access re-evaluation, claims challenge, tenant-policy change. MSAL has no way to know about any of this; it produces a clean refreshed token and calls it a day. The user is left in an endless silent 401 loop because nothing kicks them to `loginRedirect`.
136
-
137
- Adds `recoverFromHardAuthFailure(reason)` on `authStore`. The fetch interceptor calls it when a SECOND consecutive 401 fires (i.e. even the force-refresh retry didn't help). It clears the MSAL cache and starts `loginRedirect` so a brand-new session mints a token bound to current server policy.
138
-
139
- `createServiceFetch` now accepts an optional `recoverFromHardAuthFailure` callback. The default consumer (`@iloveagents/foundry-web-shell`) wires it to the new method on `authStore`. Same pattern in `tokenFetch`.
140
-
141
- Pair: `lastspace#TBD` (forwards the new option through `spacesFetch`).
142
-
143
- ## 0.1.2
144
-
145
- ### Patch Changes
146
-
147
- - 30a4346: fix(agent): retry protected fetch with `forceRefresh` on 401
148
-
149
- Closes the long-lived-tab 401 loop where the only recovery the user had was logging out or clearing localStorage.
150
-
151
- The token-fetch wrapper used to ask MSAL for a cached token and never retry. If the resource server rejected that token (claims challenge, conditional-access re-eval, audience drift, or staleness MSAL's clock-offset check missed), every subsequent request kept re-sending the same token.
152
-
153
- Fix: on 401 from a protected API, retry once with `acquireTokenSilent({ forceRefresh: true })`. If that also fails, the existing `InteractionRequiredAuthError` path bounces the user through Entra ID. Single-retry cap so we never loop. Request bodies are cloned per-attempt (ReadableStream is single-consume) so POST/PUT payloads survive the retry intact.
154
-
155
- The `acquireToken` callback type now accepts an optional `{ forceRefresh? }` argument — non-breaking; existing callbacks continue to work, but they should forward the option to their token source so the retry path actually reaches the auth backend.
156
-
157
- Pair: `lastspace#205` (forwards the option through `spacesFetch.acquireToken`).
158
-
159
- ## 0.1.1
160
-
161
- ### Patch Changes
162
-
163
- - 689d3e9: feat(sidebar): distinct file-drop affordance for nav containers
164
-
165
- `useNavItemDnd` now exposes `isFileDragOver` separately from `isDragOver`
166
- so consuming nav items can render a prominent file-drop visual (dashed
167
- primary outline + soft primary background + Upload icon) when the user
168
- is dragging native files over the container. Previously file drags
169
- shared the same subtle ring as entity-move drags, so users couldn't
170
- tell that a folder accepted external files. Applies to all five nav-
171
- item shapes (action-row leaf, button leaf, `NestedFolderItem`,
172
- `CollapsibleNavItem` with children/actions, `NavLink` fallthrough) and
173
- updates `ContainerDropZone` for visual parity.
174
-
175
- Also fixes a related regression: the capture-phase
176
- `onDragEnterCapture` / `onDragOverCapture` handlers were calling
177
- `e.stopPropagation()`, which short-circuits React's synthetic dispatch
178
- and prevented the bubble-phase `onDragEnter` (where state actually
179
- mutates) from running. Removed — `preventDefault()` alone is enough to
180
- mark the element as droppable.
package/CLAUDE.md DELETED
@@ -1 +0,0 @@
1
- @AGENTS.md