@mariozechner/pi-web-ui 0.5.44

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 (265) hide show
  1. package/README.md +252 -0
  2. package/dist/ChatPanel.d.ts +23 -0
  3. package/dist/ChatPanel.d.ts.map +1 -0
  4. package/dist/ChatPanel.js +224 -0
  5. package/dist/ChatPanel.js.map +1 -0
  6. package/dist/app.css +2 -0
  7. package/dist/components/AgentInterface.d.ts +35 -0
  8. package/dist/components/AgentInterface.d.ts.map +1 -0
  9. package/dist/components/AgentInterface.js +308 -0
  10. package/dist/components/AgentInterface.js.map +1 -0
  11. package/dist/components/AttachmentTile.d.ts +12 -0
  12. package/dist/components/AttachmentTile.d.ts.map +1 -0
  13. package/dist/components/AttachmentTile.js +114 -0
  14. package/dist/components/AttachmentTile.js.map +1 -0
  15. package/dist/components/ConsoleBlock.d.ts +11 -0
  16. package/dist/components/ConsoleBlock.d.ts.map +1 -0
  17. package/dist/components/ConsoleBlock.js +77 -0
  18. package/dist/components/ConsoleBlock.js.map +1 -0
  19. package/dist/components/Input.d.ts +26 -0
  20. package/dist/components/Input.d.ts.map +1 -0
  21. package/dist/components/Input.js +56 -0
  22. package/dist/components/Input.js.map +1 -0
  23. package/dist/components/MessageEditor.d.ts +38 -0
  24. package/dist/components/MessageEditor.d.ts.map +1 -0
  25. package/dist/components/MessageEditor.js +296 -0
  26. package/dist/components/MessageEditor.js.map +1 -0
  27. package/dist/components/MessageList.d.ts +13 -0
  28. package/dist/components/MessageList.d.ts.map +1 -0
  29. package/dist/components/MessageList.js +88 -0
  30. package/dist/components/MessageList.js.map +1 -0
  31. package/dist/components/Messages.d.ts +53 -0
  32. package/dist/components/Messages.d.ts.map +1 -0
  33. package/dist/components/Messages.js +323 -0
  34. package/dist/components/Messages.js.map +1 -0
  35. package/dist/components/ProviderKeyInput.d.ts +16 -0
  36. package/dist/components/ProviderKeyInput.d.ts.map +1 -0
  37. package/dist/components/ProviderKeyInput.js +183 -0
  38. package/dist/components/ProviderKeyInput.js.map +1 -0
  39. package/dist/components/SandboxedIframe.d.ts +63 -0
  40. package/dist/components/SandboxedIframe.d.ts.map +1 -0
  41. package/dist/components/SandboxedIframe.js +435 -0
  42. package/dist/components/SandboxedIframe.js.map +1 -0
  43. package/dist/components/StreamingMessageContainer.d.ts +17 -0
  44. package/dist/components/StreamingMessageContainer.d.ts.map +1 -0
  45. package/dist/components/StreamingMessageContainer.js +114 -0
  46. package/dist/components/StreamingMessageContainer.js.map +1 -0
  47. package/dist/dialogs/ApiKeyPromptDialog.d.ts +15 -0
  48. package/dist/dialogs/ApiKeyPromptDialog.d.ts.map +1 -0
  49. package/dist/dialogs/ApiKeyPromptDialog.js +80 -0
  50. package/dist/dialogs/ApiKeyPromptDialog.js.map +1 -0
  51. package/dist/dialogs/AttachmentOverlay.d.ts +32 -0
  52. package/dist/dialogs/AttachmentOverlay.d.ts.map +1 -0
  53. package/dist/dialogs/AttachmentOverlay.js +575 -0
  54. package/dist/dialogs/AttachmentOverlay.js.map +1 -0
  55. package/dist/dialogs/ModelSelector.d.ts +27 -0
  56. package/dist/dialogs/ModelSelector.d.ts.map +1 -0
  57. package/dist/dialogs/ModelSelector.js +334 -0
  58. package/dist/dialogs/ModelSelector.js.map +1 -0
  59. package/dist/dialogs/SettingsDialog.d.ts +31 -0
  60. package/dist/dialogs/SettingsDialog.d.ts.map +1 -0
  61. package/dist/dialogs/SettingsDialog.js +228 -0
  62. package/dist/dialogs/SettingsDialog.js.map +1 -0
  63. package/dist/index.d.ts +46 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +51 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/state/agent-session.d.ts +58 -0
  68. package/dist/state/agent-session.d.ts.map +1 -0
  69. package/dist/state/agent-session.js +252 -0
  70. package/dist/state/agent-session.js.map +1 -0
  71. package/dist/state/transports/AppTransport.d.ts +13 -0
  72. package/dist/state/transports/AppTransport.d.ts.map +1 -0
  73. package/dist/state/transports/AppTransport.js +316 -0
  74. package/dist/state/transports/AppTransport.js.map +1 -0
  75. package/dist/state/transports/ProviderTransport.d.ts +12 -0
  76. package/dist/state/transports/ProviderTransport.d.ts.map +1 -0
  77. package/dist/state/transports/ProviderTransport.js +44 -0
  78. package/dist/state/transports/ProviderTransport.js.map +1 -0
  79. package/dist/state/transports/index.d.ts +4 -0
  80. package/dist/state/transports/index.d.ts.map +1 -0
  81. package/dist/state/transports/index.js +4 -0
  82. package/dist/state/transports/index.js.map +1 -0
  83. package/dist/state/transports/proxy-types.d.ts +48 -0
  84. package/dist/state/transports/proxy-types.d.ts.map +1 -0
  85. package/dist/state/transports/proxy-types.js +2 -0
  86. package/dist/state/transports/proxy-types.js.map +1 -0
  87. package/dist/state/transports/types.d.ts +11 -0
  88. package/dist/state/transports/types.d.ts.map +1 -0
  89. package/dist/state/transports/types.js +2 -0
  90. package/dist/state/transports/types.js.map +1 -0
  91. package/dist/state/types.d.ts +15 -0
  92. package/dist/state/types.d.ts.map +1 -0
  93. package/dist/state/types.js +2 -0
  94. package/dist/state/types.js.map +1 -0
  95. package/dist/storage/app-storage.d.ts +26 -0
  96. package/dist/storage/app-storage.d.ts.map +1 -0
  97. package/dist/storage/app-storage.js +44 -0
  98. package/dist/storage/app-storage.js.map +1 -0
  99. package/dist/storage/backends/chrome-storage-backend.d.ts +18 -0
  100. package/dist/storage/backends/chrome-storage-backend.d.ts.map +1 -0
  101. package/dist/storage/backends/chrome-storage-backend.js +67 -0
  102. package/dist/storage/backends/chrome-storage-backend.js.map +1 -0
  103. package/dist/storage/backends/indexeddb-backend.d.ts +20 -0
  104. package/dist/storage/backends/indexeddb-backend.d.ts.map +1 -0
  105. package/dist/storage/backends/indexeddb-backend.js +89 -0
  106. package/dist/storage/backends/indexeddb-backend.js.map +1 -0
  107. package/dist/storage/backends/local-storage-backend.d.ts +18 -0
  108. package/dist/storage/backends/local-storage-backend.d.ts.map +1 -0
  109. package/dist/storage/backends/local-storage-backend.js +69 -0
  110. package/dist/storage/backends/local-storage-backend.js.map +1 -0
  111. package/dist/storage/repositories/provider-keys-repository.d.ts +34 -0
  112. package/dist/storage/repositories/provider-keys-repository.d.ts.map +1 -0
  113. package/dist/storage/repositories/provider-keys-repository.js +50 -0
  114. package/dist/storage/repositories/provider-keys-repository.js.map +1 -0
  115. package/dist/storage/repositories/settings-repository.d.ts +34 -0
  116. package/dist/storage/repositories/settings-repository.d.ts.map +1 -0
  117. package/dist/storage/repositories/settings-repository.js +46 -0
  118. package/dist/storage/repositories/settings-repository.js.map +1 -0
  119. package/dist/storage/types.d.ts +43 -0
  120. package/dist/storage/types.d.ts.map +1 -0
  121. package/dist/storage/types.js +2 -0
  122. package/dist/storage/types.js.map +1 -0
  123. package/dist/tools/artifacts/ArtifactElement.d.ts +10 -0
  124. package/dist/tools/artifacts/ArtifactElement.d.ts.map +1 -0
  125. package/dist/tools/artifacts/ArtifactElement.js +12 -0
  126. package/dist/tools/artifacts/ArtifactElement.js.map +1 -0
  127. package/dist/tools/artifacts/HtmlArtifact.d.ts +30 -0
  128. package/dist/tools/artifacts/HtmlArtifact.d.ts.map +1 -0
  129. package/dist/tools/artifacts/HtmlArtifact.js +217 -0
  130. package/dist/tools/artifacts/HtmlArtifact.js.map +1 -0
  131. package/dist/tools/artifacts/MarkdownArtifact.d.ts +20 -0
  132. package/dist/tools/artifacts/MarkdownArtifact.d.ts.map +1 -0
  133. package/dist/tools/artifacts/MarkdownArtifact.js +84 -0
  134. package/dist/tools/artifacts/MarkdownArtifact.js.map +1 -0
  135. package/dist/tools/artifacts/SvgArtifact.d.ts +19 -0
  136. package/dist/tools/artifacts/SvgArtifact.d.ts.map +1 -0
  137. package/dist/tools/artifacts/SvgArtifact.js +80 -0
  138. package/dist/tools/artifacts/SvgArtifact.js.map +1 -0
  139. package/dist/tools/artifacts/TextArtifact.d.ts +20 -0
  140. package/dist/tools/artifacts/TextArtifact.d.ts.map +1 -0
  141. package/dist/tools/artifacts/TextArtifact.js +147 -0
  142. package/dist/tools/artifacts/TextArtifact.js.map +1 -0
  143. package/dist/tools/artifacts/artifacts.d.ts +67 -0
  144. package/dist/tools/artifacts/artifacts.d.ts.map +1 -0
  145. package/dist/tools/artifacts/artifacts.js +836 -0
  146. package/dist/tools/artifacts/artifacts.js.map +1 -0
  147. package/dist/tools/artifacts/index.d.ts +7 -0
  148. package/dist/tools/artifacts/index.d.ts.map +1 -0
  149. package/dist/tools/artifacts/index.js +7 -0
  150. package/dist/tools/artifacts/index.js.map +1 -0
  151. package/dist/tools/index.d.ts +14 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +29 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/javascript-repl.d.ts +43 -0
  156. package/dist/tools/javascript-repl.d.ts.map +1 -0
  157. package/dist/tools/javascript-repl.js +252 -0
  158. package/dist/tools/javascript-repl.js.map +1 -0
  159. package/dist/tools/renderer-registry.d.ts +11 -0
  160. package/dist/tools/renderer-registry.d.ts.map +1 -0
  161. package/dist/tools/renderer-registry.js +15 -0
  162. package/dist/tools/renderer-registry.js.map +1 -0
  163. package/dist/tools/renderers/BashRenderer.d.ts +12 -0
  164. package/dist/tools/renderers/BashRenderer.d.ts.map +1 -0
  165. package/dist/tools/renderers/BashRenderer.js +35 -0
  166. package/dist/tools/renderers/BashRenderer.js.map +1 -0
  167. package/dist/tools/renderers/CalculateRenderer.d.ts +12 -0
  168. package/dist/tools/renderers/CalculateRenderer.d.ts.map +1 -0
  169. package/dist/tools/renderers/CalculateRenderer.js +38 -0
  170. package/dist/tools/renderers/CalculateRenderer.js.map +1 -0
  171. package/dist/tools/renderers/DefaultRenderer.d.ts +8 -0
  172. package/dist/tools/renderers/DefaultRenderer.d.ts.map +1 -0
  173. package/dist/tools/renderers/DefaultRenderer.js +31 -0
  174. package/dist/tools/renderers/DefaultRenderer.js.map +1 -0
  175. package/dist/tools/renderers/GetCurrentTimeRenderer.d.ts +12 -0
  176. package/dist/tools/renderers/GetCurrentTimeRenderer.d.ts.map +1 -0
  177. package/dist/tools/renderers/GetCurrentTimeRenderer.js +30 -0
  178. package/dist/tools/renderers/GetCurrentTimeRenderer.js.map +1 -0
  179. package/dist/tools/types.d.ts +7 -0
  180. package/dist/tools/types.d.ts.map +1 -0
  181. package/dist/tools/types.js +2 -0
  182. package/dist/tools/types.js.map +1 -0
  183. package/dist/utils/attachment-utils.d.ts +19 -0
  184. package/dist/utils/attachment-utils.d.ts.map +1 -0
  185. package/dist/utils/attachment-utils.js +415 -0
  186. package/dist/utils/attachment-utils.js.map +1 -0
  187. package/dist/utils/auth-token.d.ts +3 -0
  188. package/dist/utils/auth-token.d.ts.map +1 -0
  189. package/dist/utils/auth-token.js +19 -0
  190. package/dist/utils/auth-token.js.map +1 -0
  191. package/dist/utils/format.d.ts +6 -0
  192. package/dist/utils/format.d.ts.map +1 -0
  193. package/dist/utils/format.js +47 -0
  194. package/dist/utils/format.js.map +1 -0
  195. package/dist/utils/i18n.d.ts +111 -0
  196. package/dist/utils/i18n.d.ts.map +1 -0
  197. package/dist/utils/i18n.js +224 -0
  198. package/dist/utils/i18n.js.map +1 -0
  199. package/dist/utils/test-sessions.d.ts +347 -0
  200. package/dist/utils/test-sessions.d.ts.map +1 -0
  201. package/dist/utils/test-sessions.js +2215 -0
  202. package/dist/utils/test-sessions.js.map +1 -0
  203. package/example/README.md +61 -0
  204. package/example/index.html +13 -0
  205. package/example/package-lock.json +1965 -0
  206. package/example/package.json +22 -0
  207. package/example/src/app.css +1 -0
  208. package/example/src/main.ts +57 -0
  209. package/example/src/test-sessions.ts +104 -0
  210. package/example/tsconfig.json +15 -0
  211. package/example/vite.config.ts +6 -0
  212. package/package.json +45 -0
  213. package/src/ChatPanel.ts +214 -0
  214. package/src/app.css +44 -0
  215. package/src/components/AgentInterface.ts +316 -0
  216. package/src/components/AttachmentTile.ts +112 -0
  217. package/src/components/ConsoleBlock.ts +67 -0
  218. package/src/components/Input.ts +112 -0
  219. package/src/components/MessageEditor.ts +272 -0
  220. package/src/components/MessageList.ts +82 -0
  221. package/src/components/Messages.ts +310 -0
  222. package/src/components/ProviderKeyInput.ts +170 -0
  223. package/src/components/SandboxedIframe.ts +525 -0
  224. package/src/components/StreamingMessageContainer.ts +101 -0
  225. package/src/dialogs/ApiKeyPromptDialog.ts +76 -0
  226. package/src/dialogs/AttachmentOverlay.ts +635 -0
  227. package/src/dialogs/ModelSelector.ts +324 -0
  228. package/src/dialogs/SettingsDialog.ts +223 -0
  229. package/src/index.ts +63 -0
  230. package/src/state/agent-session.ts +311 -0
  231. package/src/state/transports/AppTransport.ts +363 -0
  232. package/src/state/transports/ProviderTransport.ts +49 -0
  233. package/src/state/transports/index.ts +3 -0
  234. package/src/state/transports/proxy-types.ts +15 -0
  235. package/src/state/transports/types.ts +16 -0
  236. package/src/state/types.ts +11 -0
  237. package/src/storage/app-storage.ts +53 -0
  238. package/src/storage/backends/chrome-storage-backend.ts +82 -0
  239. package/src/storage/backends/indexeddb-backend.ts +107 -0
  240. package/src/storage/backends/local-storage-backend.ts +74 -0
  241. package/src/storage/repositories/provider-keys-repository.ts +55 -0
  242. package/src/storage/repositories/settings-repository.ts +51 -0
  243. package/src/storage/types.ts +48 -0
  244. package/src/tools/artifacts/ArtifactElement.ts +15 -0
  245. package/src/tools/artifacts/HtmlArtifact.ts +221 -0
  246. package/src/tools/artifacts/MarkdownArtifact.ts +81 -0
  247. package/src/tools/artifacts/SvgArtifact.ts +77 -0
  248. package/src/tools/artifacts/TextArtifact.ts +148 -0
  249. package/src/tools/artifacts/artifacts.ts +888 -0
  250. package/src/tools/artifacts/index.ts +6 -0
  251. package/src/tools/index.ts +35 -0
  252. package/src/tools/javascript-repl.ts +309 -0
  253. package/src/tools/renderer-registry.ts +18 -0
  254. package/src/tools/renderers/BashRenderer.ts +45 -0
  255. package/src/tools/renderers/CalculateRenderer.ts +49 -0
  256. package/src/tools/renderers/DefaultRenderer.ts +36 -0
  257. package/src/tools/renderers/GetCurrentTimeRenderer.ts +39 -0
  258. package/src/tools/types.ts +7 -0
  259. package/src/utils/attachment-utils.ts +472 -0
  260. package/src/utils/auth-token.ts +22 -0
  261. package/src/utils/format.ts +42 -0
  262. package/src/utils/i18n.ts +343 -0
  263. package/src/utils/test-sessions.ts +2247 -0
  264. package/tsconfig.build.json +20 -0
  265. package/tsconfig.json +7 -0
@@ -0,0 +1,311 @@
1
+ import type { Context } from "@mariozechner/pi-ai";
2
+ import {
3
+ type AgentTool,
4
+ type AssistantMessage as AssistantMessageType,
5
+ getModel,
6
+ type ImageContent,
7
+ type Message,
8
+ type Model,
9
+ type TextContent,
10
+ } from "@mariozechner/pi-ai";
11
+ import type { AppMessage } from "../components/Messages.js";
12
+ import type { Attachment } from "../utils/attachment-utils.js";
13
+ import { AppTransport } from "./transports/AppTransport.js";
14
+ import { ProviderTransport } from "./transports/ProviderTransport.js";
15
+ import type { AgentRunConfig, AgentTransport } from "./transports/types.js";
16
+ import type { DebugLogEntry } from "./types.js";
17
+
18
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high";
19
+
20
+ export interface AgentSessionState {
21
+ id: string;
22
+ systemPrompt: string;
23
+ model: Model<any> | null;
24
+ thinkingLevel: ThinkingLevel;
25
+ tools: AgentTool<any>[];
26
+ messages: AppMessage[];
27
+ isStreaming: boolean;
28
+ streamMessage: Message | null;
29
+ pendingToolCalls: Set<string>;
30
+ error?: string;
31
+ }
32
+
33
+ export type AgentSessionEvent =
34
+ | { type: "state-update"; state: AgentSessionState }
35
+ | { type: "error-no-model" }
36
+ | { type: "error-no-api-key"; provider: string };
37
+
38
+ export type TransportMode = "provider" | "app";
39
+
40
+ export interface AgentSessionOptions {
41
+ initialState?: Partial<AgentSessionState>;
42
+ messagePreprocessor?: (messages: AppMessage[]) => Promise<Message[]>;
43
+ debugListener?: (entry: DebugLogEntry) => void;
44
+ transportMode?: TransportMode;
45
+ authTokenProvider?: () => Promise<string | undefined>;
46
+ }
47
+
48
+ export class AgentSession {
49
+ private _state: AgentSessionState = {
50
+ id: "default",
51
+ systemPrompt: "",
52
+ model: getModel("google", "gemini-2.5-flash-lite-preview-06-17"),
53
+ thinkingLevel: "off",
54
+ tools: [],
55
+ messages: [],
56
+ isStreaming: false,
57
+ streamMessage: null,
58
+ pendingToolCalls: new Set<string>(),
59
+ error: undefined,
60
+ };
61
+ private listeners = new Set<(e: AgentSessionEvent) => void>();
62
+ private abortController?: AbortController;
63
+ private transport: AgentTransport;
64
+ private messagePreprocessor?: (messages: AppMessage[]) => Promise<Message[]>;
65
+ private debugListener?: (entry: DebugLogEntry) => void;
66
+
67
+ constructor(opts: AgentSessionOptions = {}) {
68
+ this._state = { ...this._state, ...opts.initialState };
69
+ this.messagePreprocessor = opts.messagePreprocessor;
70
+ this.debugListener = opts.debugListener;
71
+
72
+ const mode = opts.transportMode || "provider";
73
+
74
+ if (mode === "app") {
75
+ this.transport = new AppTransport(async () => this.preprocessMessages());
76
+ } else {
77
+ this.transport = new ProviderTransport(async () => this.preprocessMessages());
78
+ }
79
+ }
80
+
81
+ private async preprocessMessages(): Promise<Message[]> {
82
+ const filtered = this._state.messages.map((m) => {
83
+ if (m.role === "user") {
84
+ const { attachments, ...rest } = m as AppMessage & { attachments?: Attachment[] };
85
+ return rest;
86
+ }
87
+ return m;
88
+ });
89
+ return this.messagePreprocessor ? this.messagePreprocessor(filtered as AppMessage[]) : (filtered as Message[]);
90
+ }
91
+
92
+ get state(): AgentSessionState {
93
+ return this._state;
94
+ }
95
+
96
+ subscribe(fn: (e: AgentSessionEvent) => void): () => void {
97
+ this.listeners.add(fn);
98
+ fn({ type: "state-update", state: this._state });
99
+ return () => this.listeners.delete(fn);
100
+ }
101
+
102
+ // Mutators
103
+ setSystemPrompt(v: string) {
104
+ this.patch({ systemPrompt: v });
105
+ }
106
+ setModel(m: Model<any> | null) {
107
+ this.patch({ model: m });
108
+ }
109
+ setThinkingLevel(l: ThinkingLevel) {
110
+ this.patch({ thinkingLevel: l });
111
+ }
112
+ setTools(t: AgentTool<any>[]) {
113
+ this.patch({ tools: t });
114
+ }
115
+ replaceMessages(ms: AppMessage[]) {
116
+ this.patch({ messages: ms.slice() });
117
+ }
118
+ appendMessage(m: AppMessage) {
119
+ this.patch({ messages: [...this._state.messages, m] });
120
+ }
121
+ clearMessages() {
122
+ this.patch({ messages: [] });
123
+ }
124
+
125
+ abort() {
126
+ this.abortController?.abort();
127
+ }
128
+
129
+ async prompt(input: string, attachments?: Attachment[]) {
130
+ const model = this._state.model;
131
+ if (!model) {
132
+ this.emit({ type: "error-no-model" });
133
+ return;
134
+ }
135
+
136
+ // Build user message with attachments
137
+ const content: Array<TextContent | ImageContent> = [{ type: "text", text: input }];
138
+ if (attachments?.length) {
139
+ for (const a of attachments) {
140
+ if (a.type === "image") {
141
+ content.push({ type: "image", data: a.content, mimeType: a.mimeType });
142
+ } else if (a.type === "document" && a.extractedText) {
143
+ content.push({
144
+ type: "text",
145
+ text: `\n\n[Document: ${a.fileName}]\n${a.extractedText}`,
146
+ isDocument: true,
147
+ } as TextContent);
148
+ }
149
+ }
150
+ }
151
+
152
+ const userMessage: AppMessage = {
153
+ role: "user",
154
+ content,
155
+ attachments: attachments?.length ? attachments : undefined,
156
+ };
157
+
158
+ this.abortController = new AbortController();
159
+ this.patch({ isStreaming: true, streamMessage: null, error: undefined });
160
+
161
+ const reasoning =
162
+ this._state.thinkingLevel === "off"
163
+ ? undefined
164
+ : this._state.thinkingLevel === "minimal"
165
+ ? "low"
166
+ : this._state.thinkingLevel;
167
+ const cfg: AgentRunConfig = {
168
+ systemPrompt: this._state.systemPrompt,
169
+ tools: this._state.tools,
170
+ model,
171
+ reasoning,
172
+ };
173
+
174
+ try {
175
+ let partial: Message | null = null;
176
+ let turnDebug: DebugLogEntry | null = null;
177
+ let turnStart = 0;
178
+ for await (const ev of this.transport.run(userMessage as Message, cfg, this.abortController.signal)) {
179
+ switch (ev.type) {
180
+ case "turn_start": {
181
+ turnStart = performance.now();
182
+ // Build request context snapshot
183
+ const existing = this._state.messages as Message[];
184
+ const ctx: Context = {
185
+ systemPrompt: this._state.systemPrompt,
186
+ messages: [...existing],
187
+ tools: this._state.tools,
188
+ };
189
+ turnDebug = {
190
+ timestamp: new Date().toISOString(),
191
+ request: {
192
+ provider: cfg.model.provider,
193
+ model: cfg.model.id,
194
+ context: { ...ctx },
195
+ },
196
+ sseEvents: [],
197
+ };
198
+ break;
199
+ }
200
+ case "message_start":
201
+ case "message_update": {
202
+ partial = ev.message;
203
+ // Collect SSE-like events for debug (drop heavy partial)
204
+ if (ev.type === "message_update" && ev.assistantMessageEvent && turnDebug) {
205
+ const copy: any = { ...ev.assistantMessageEvent };
206
+ if (copy && "partial" in copy) delete copy.partial;
207
+ turnDebug.sseEvents.push(JSON.stringify(copy));
208
+ if (!turnDebug.ttft) turnDebug.ttft = performance.now() - turnStart;
209
+ }
210
+ this.patch({ streamMessage: ev.message });
211
+ break;
212
+ }
213
+ case "message_end": {
214
+ partial = null;
215
+ this.appendMessage(ev.message as AppMessage);
216
+ this.patch({ streamMessage: null });
217
+ if (turnDebug) {
218
+ if (ev.message.role !== "assistant" && ev.message.role !== "toolResult") {
219
+ turnDebug.request.context.messages.push(ev.message);
220
+ }
221
+ if (ev.message.role === "assistant") turnDebug.response = ev.message as any;
222
+ }
223
+ break;
224
+ }
225
+ case "tool_execution_start": {
226
+ const s = new Set(this._state.pendingToolCalls);
227
+ s.add(ev.toolCallId);
228
+ this.patch({ pendingToolCalls: s });
229
+ break;
230
+ }
231
+ case "tool_execution_end": {
232
+ const s = new Set(this._state.pendingToolCalls);
233
+ s.delete(ev.toolCallId);
234
+ this.patch({ pendingToolCalls: s });
235
+ break;
236
+ }
237
+ case "turn_end": {
238
+ // finalize current turn
239
+ if (turnDebug) {
240
+ turnDebug.totalTime = performance.now() - turnStart;
241
+ this.debugListener?.(turnDebug);
242
+ turnDebug = null;
243
+ }
244
+ break;
245
+ }
246
+ case "agent_end": {
247
+ this.patch({ streamMessage: null });
248
+ break;
249
+ }
250
+ }
251
+ }
252
+
253
+ if (partial && partial.role === "assistant" && partial.content.length > 0) {
254
+ const onlyEmpty = !partial.content.some(
255
+ (c) =>
256
+ (c.type === "thinking" && c.thinking.trim().length > 0) ||
257
+ (c.type === "text" && c.text.trim().length > 0) ||
258
+ (c.type === "toolCall" && c.name.trim().length > 0),
259
+ );
260
+ if (!onlyEmpty) {
261
+ this.appendMessage(partial as AppMessage);
262
+ } else {
263
+ if (this.abortController?.signal.aborted) {
264
+ throw new Error("Request was aborted");
265
+ }
266
+ }
267
+ }
268
+ } catch (err: any) {
269
+ if (String(err?.message || err) === "no-api-key") {
270
+ this.emit({ type: "error-no-api-key", provider: model.provider });
271
+ } else {
272
+ const msg: AssistantMessageType = {
273
+ role: "assistant",
274
+ content: [{ type: "text", text: "" }],
275
+ api: model.api,
276
+ provider: model.provider,
277
+ model: model.id,
278
+ usage: {
279
+ input: 0,
280
+ output: 0,
281
+ cacheRead: 0,
282
+ cacheWrite: 0,
283
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
284
+ },
285
+ stopReason: this.abortController?.signal.aborted ? "aborted" : "error",
286
+ errorMessage: err?.message || String(err),
287
+ };
288
+ this.appendMessage(msg as AppMessage);
289
+ this.patch({ error: err?.message || String(err) });
290
+ }
291
+ } finally {
292
+ this.patch({ isStreaming: false, streamMessage: null, pendingToolCalls: new Set<string>() });
293
+ this.abortController = undefined;
294
+ }
295
+ {
296
+ const { systemPrompt, model, messages } = this._state;
297
+ console.log("final state:", { systemPrompt, model, messages });
298
+ }
299
+ }
300
+
301
+ private patch(p: Partial<AgentSessionState>): void {
302
+ this._state = { ...this._state, ...p };
303
+ this.emit({ type: "state-update", state: this._state });
304
+ }
305
+
306
+ private emit(e: AgentSessionEvent) {
307
+ for (const listener of this.listeners) {
308
+ listener(e);
309
+ }
310
+ }
311
+ }
@@ -0,0 +1,363 @@
1
+ import type {
2
+ AgentContext,
3
+ Api,
4
+ AssistantMessage,
5
+ AssistantMessageEvent,
6
+ Context,
7
+ Message,
8
+ Model,
9
+ PromptConfig,
10
+ SimpleStreamOptions,
11
+ ToolCall,
12
+ UserMessage,
13
+ } from "@mariozechner/pi-ai";
14
+ import { agentLoop } from "@mariozechner/pi-ai";
15
+ import { AssistantMessageEventStream } from "@mariozechner/pi-ai/dist/utils/event-stream.js";
16
+ import { parseStreamingJson } from "@mariozechner/pi-ai/dist/utils/json-parse.js";
17
+ import { clearAuthToken, getAuthToken } from "../../utils/auth-token.js";
18
+ import { i18n } from "../../utils/i18n.js";
19
+ import type { ProxyAssistantMessageEvent } from "./proxy-types.js";
20
+ import type { AgentRunConfig, AgentTransport } from "./types.js";
21
+
22
+ /**
23
+ * Stream function that proxies through a server instead of calling providers directly.
24
+ * The server strips the partial field from delta events to reduce bandwidth.
25
+ * We reconstruct the partial message client-side.
26
+ */
27
+ function streamSimpleProxy(
28
+ model: Model<any>,
29
+ context: Context,
30
+ options: SimpleStreamOptions & { authToken: string },
31
+ proxyUrl: string,
32
+ ): AssistantMessageEventStream {
33
+ const stream = new AssistantMessageEventStream();
34
+
35
+ (async () => {
36
+ // Initialize the partial message that we'll build up from events
37
+ const partial: AssistantMessage = {
38
+ role: "assistant",
39
+ stopReason: "stop",
40
+ content: [],
41
+ api: model.api,
42
+ provider: model.provider,
43
+ model: model.id,
44
+ usage: {
45
+ input: 0,
46
+ output: 0,
47
+ cacheRead: 0,
48
+ cacheWrite: 0,
49
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
50
+ },
51
+ };
52
+
53
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
54
+
55
+ // Set up abort handler to cancel the reader
56
+ const abortHandler = () => {
57
+ if (reader) {
58
+ reader.cancel("Request aborted by user").catch(() => {});
59
+ }
60
+ };
61
+
62
+ if (options.signal) {
63
+ options.signal.addEventListener("abort", abortHandler);
64
+ }
65
+
66
+ try {
67
+ const response = await fetch(`${proxyUrl}/api/stream`, {
68
+ method: "POST",
69
+ headers: {
70
+ Authorization: `Bearer ${options.authToken}`,
71
+ "Content-Type": "application/json",
72
+ },
73
+ body: JSON.stringify({
74
+ model,
75
+ context,
76
+ options: {
77
+ temperature: options.temperature,
78
+ maxTokens: options.maxTokens,
79
+ reasoning: options.reasoning,
80
+ // Don't send apiKey or signal - those are added server-side
81
+ },
82
+ }),
83
+ signal: options.signal,
84
+ });
85
+
86
+ if (!response.ok) {
87
+ let errorMessage = `Proxy error: ${response.status} ${response.statusText}`;
88
+ try {
89
+ const errorData = await response.json();
90
+ if (errorData.error) {
91
+ errorMessage = `Proxy error: ${errorData.error}`;
92
+ }
93
+ } catch {
94
+ // Couldn't parse error response, use default message
95
+ }
96
+ throw new Error(errorMessage);
97
+ }
98
+
99
+ // Parse SSE stream
100
+ reader = response.body!.getReader();
101
+ const decoder = new TextDecoder();
102
+ let buffer = "";
103
+
104
+ while (true) {
105
+ const { done, value } = await reader.read();
106
+ if (done) break;
107
+
108
+ // Check if aborted after reading
109
+ if (options.signal?.aborted) {
110
+ throw new Error("Request aborted by user");
111
+ }
112
+
113
+ buffer += decoder.decode(value, { stream: true });
114
+ const lines = buffer.split("\n");
115
+ buffer = lines.pop() || "";
116
+
117
+ for (const line of lines) {
118
+ if (line.startsWith("data: ")) {
119
+ const data = line.slice(6).trim();
120
+ if (data) {
121
+ const proxyEvent = JSON.parse(data) as ProxyAssistantMessageEvent;
122
+ let event: AssistantMessageEvent | undefined;
123
+
124
+ // Handle different event types
125
+ // Server sends events with partial for non-delta events,
126
+ // and without partial for delta events
127
+ switch (proxyEvent.type) {
128
+ case "start":
129
+ event = { type: "start", partial };
130
+ break;
131
+
132
+ case "text_start":
133
+ partial.content[proxyEvent.contentIndex] = {
134
+ type: "text",
135
+ text: "",
136
+ };
137
+ event = { type: "text_start", contentIndex: proxyEvent.contentIndex, partial };
138
+ break;
139
+
140
+ case "text_delta": {
141
+ const content = partial.content[proxyEvent.contentIndex];
142
+ if (content?.type === "text") {
143
+ content.text += proxyEvent.delta;
144
+ event = {
145
+ type: "text_delta",
146
+ contentIndex: proxyEvent.contentIndex,
147
+ delta: proxyEvent.delta,
148
+ partial,
149
+ };
150
+ } else {
151
+ throw new Error("Received text_delta for non-text content");
152
+ }
153
+ break;
154
+ }
155
+ case "text_end": {
156
+ const content = partial.content[proxyEvent.contentIndex];
157
+ if (content?.type === "text") {
158
+ content.textSignature = proxyEvent.contentSignature;
159
+ event = {
160
+ type: "text_end",
161
+ contentIndex: proxyEvent.contentIndex,
162
+ content: content.text,
163
+ partial,
164
+ };
165
+ } else {
166
+ throw new Error("Received text_end for non-text content");
167
+ }
168
+ break;
169
+ }
170
+
171
+ case "thinking_start":
172
+ partial.content[proxyEvent.contentIndex] = {
173
+ type: "thinking",
174
+ thinking: "",
175
+ };
176
+ event = { type: "thinking_start", contentIndex: proxyEvent.contentIndex, partial };
177
+ break;
178
+
179
+ case "thinking_delta": {
180
+ const content = partial.content[proxyEvent.contentIndex];
181
+ if (content?.type === "thinking") {
182
+ content.thinking += proxyEvent.delta;
183
+ event = {
184
+ type: "thinking_delta",
185
+ contentIndex: proxyEvent.contentIndex,
186
+ delta: proxyEvent.delta,
187
+ partial,
188
+ };
189
+ } else {
190
+ throw new Error("Received thinking_delta for non-thinking content");
191
+ }
192
+ break;
193
+ }
194
+
195
+ case "thinking_end": {
196
+ const content = partial.content[proxyEvent.contentIndex];
197
+ if (content?.type === "thinking") {
198
+ content.thinkingSignature = proxyEvent.contentSignature;
199
+ event = {
200
+ type: "thinking_end",
201
+ contentIndex: proxyEvent.contentIndex,
202
+ content: content.thinking,
203
+ partial,
204
+ };
205
+ } else {
206
+ throw new Error("Received thinking_end for non-thinking content");
207
+ }
208
+ break;
209
+ }
210
+
211
+ case "toolcall_start":
212
+ partial.content[proxyEvent.contentIndex] = {
213
+ type: "toolCall",
214
+ id: proxyEvent.id,
215
+ name: proxyEvent.toolName,
216
+ arguments: {},
217
+ partialJson: "",
218
+ } satisfies ToolCall & { partialJson: string } as ToolCall;
219
+ event = { type: "toolcall_start", contentIndex: proxyEvent.contentIndex, partial };
220
+ break;
221
+
222
+ case "toolcall_delta": {
223
+ const content = partial.content[proxyEvent.contentIndex];
224
+ if (content?.type === "toolCall") {
225
+ (content as any).partialJson += proxyEvent.delta;
226
+ content.arguments = parseStreamingJson((content as any).partialJson) || {};
227
+ event = {
228
+ type: "toolcall_delta",
229
+ contentIndex: proxyEvent.contentIndex,
230
+ delta: proxyEvent.delta,
231
+ partial,
232
+ };
233
+ partial.content[proxyEvent.contentIndex] = { ...content }; // Trigger reactivity
234
+ } else {
235
+ throw new Error("Received toolcall_delta for non-toolCall content");
236
+ }
237
+ break;
238
+ }
239
+
240
+ case "toolcall_end": {
241
+ const content = partial.content[proxyEvent.contentIndex];
242
+ if (content?.type === "toolCall") {
243
+ delete (content as any).partialJson;
244
+ event = {
245
+ type: "toolcall_end",
246
+ contentIndex: proxyEvent.contentIndex,
247
+ toolCall: content,
248
+ partial,
249
+ };
250
+ }
251
+ break;
252
+ }
253
+
254
+ case "done":
255
+ partial.stopReason = proxyEvent.reason;
256
+ partial.usage = proxyEvent.usage;
257
+ event = { type: "done", reason: proxyEvent.reason, message: partial };
258
+ break;
259
+
260
+ case "error":
261
+ partial.stopReason = proxyEvent.reason;
262
+ partial.errorMessage = proxyEvent.errorMessage;
263
+ partial.usage = proxyEvent.usage;
264
+ event = { type: "error", reason: proxyEvent.reason, error: partial };
265
+ break;
266
+
267
+ default: {
268
+ // Exhaustive check
269
+ const _exhaustiveCheck: never = proxyEvent;
270
+ console.warn(`Unhandled event type: ${(proxyEvent as any).type}`);
271
+ break;
272
+ }
273
+ }
274
+
275
+ // Push the event to stream
276
+ if (event) {
277
+ stream.push(event);
278
+ } else {
279
+ throw new Error("Failed to create event from proxy event");
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ // Check if aborted after reading
287
+ if (options.signal?.aborted) {
288
+ throw new Error("Request aborted by user");
289
+ }
290
+
291
+ stream.end();
292
+ } catch (error) {
293
+ const errorMessage = error instanceof Error ? error.message : String(error);
294
+ if (errorMessage.toLowerCase().includes("proxy") && errorMessage.includes("Unauthorized")) {
295
+ clearAuthToken();
296
+ }
297
+ partial.stopReason = options.signal?.aborted ? "aborted" : "error";
298
+ partial.errorMessage = errorMessage;
299
+ stream.push({
300
+ type: "error",
301
+ reason: partial.stopReason,
302
+ error: partial,
303
+ } satisfies AssistantMessageEvent);
304
+ stream.end();
305
+ } finally {
306
+ // Clean up abort handler
307
+ if (options.signal) {
308
+ options.signal.removeEventListener("abort", abortHandler);
309
+ }
310
+ }
311
+ })();
312
+
313
+ return stream;
314
+ }
315
+
316
+ // Proxy transport executes the turn using a remote proxy server
317
+ /**
318
+ * Transport that uses an app server with user authentication tokens.
319
+ * The server manages user accounts and proxies requests to LLM providers.
320
+ */
321
+ export class AppTransport implements AgentTransport {
322
+ // Hardcoded proxy URL for now - will be made configurable later
323
+ private readonly proxyUrl = "https://genai.mariozechner.at";
324
+
325
+ constructor(private readonly getMessages: () => Promise<Message[]>) {}
326
+
327
+ async *run(userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal) {
328
+ const authToken = await getAuthToken();
329
+ if (!authToken) {
330
+ throw new Error(i18n("Auth token is required for proxy transport"));
331
+ }
332
+
333
+ // Use proxy - no local API key needed
334
+ const streamFn = <TApi extends Api>(model: Model<TApi>, context: Context, options?: SimpleStreamOptions) => {
335
+ return streamSimpleProxy(
336
+ model,
337
+ context,
338
+ {
339
+ ...options,
340
+ authToken,
341
+ },
342
+ this.proxyUrl,
343
+ );
344
+ };
345
+
346
+ const context: AgentContext = {
347
+ systemPrompt: cfg.systemPrompt,
348
+ messages: await this.getMessages(),
349
+ tools: cfg.tools,
350
+ };
351
+
352
+ const pc: PromptConfig = {
353
+ model: cfg.model,
354
+ reasoning: cfg.reasoning,
355
+ };
356
+
357
+ // Yield events from the upstream agentLoop iterator
358
+ // Pass streamFn as the 5th parameter to use proxy
359
+ for await (const ev of agentLoop(userMessage as unknown as UserMessage, context, pc, signal, streamFn as any)) {
360
+ yield ev;
361
+ }
362
+ }
363
+ }