iterate 0.2.7 → 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 (73) hide show
  1. package/README.md +173 -81
  2. package/dist/api.d.ts +643 -0
  3. package/dist/api.mjs +0 -0
  4. package/dist/app-server.d.ts +51 -0
  5. package/dist/app-server.mjs +481 -0
  6. package/dist/app-server.mjs.map +1 -0
  7. package/dist/app-session.d.ts +49 -0
  8. package/dist/app-session.mjs +235 -0
  9. package/dist/app-session.mjs.map +1 -0
  10. package/dist/app.d.ts +29 -0
  11. package/dist/app.mjs +180 -0
  12. package/dist/app.mjs.map +1 -0
  13. package/dist/client/live-state.d.ts +63 -0
  14. package/dist/client/oauth.d.ts +17 -0
  15. package/dist/client/react.d.ts +77 -0
  16. package/dist/client/socket.d.ts +7 -0
  17. package/dist/client.mjs +156 -0
  18. package/dist/client.mjs.map +1 -0
  19. package/dist/expression.d.ts +88 -0
  20. package/dist/expression.mjs +301 -0
  21. package/dist/expression.mjs.map +1 -0
  22. package/dist/lib-BWr-5mFO.mjs +36 -0
  23. package/dist/lib-BWr-5mFO.mjs.map +1 -0
  24. package/dist/lib.d.ts +70 -0
  25. package/dist/lib.mjs +228 -0
  26. package/dist/lib.mjs.map +1 -0
  27. package/dist/node.d.ts +15 -0
  28. package/dist/node.mjs +47 -0
  29. package/dist/node.mjs.map +1 -0
  30. package/dist/oauth-scopes.d.ts +32 -0
  31. package/dist/oauth-scopes.mjs +40 -0
  32. package/dist/oauth-scopes.mjs.map +1 -0
  33. package/dist/oauth.mjs +41 -0
  34. package/dist/oauth.mjs.map +1 -0
  35. package/dist/principal.d.ts +8 -0
  36. package/dist/principal.mjs +8 -0
  37. package/dist/principal.mjs.map +1 -0
  38. package/dist/project-ingress.d.ts +58 -0
  39. package/dist/project-ingress.mjs +104 -0
  40. package/dist/project-ingress.mjs.map +1 -0
  41. package/dist/react.mjs +285 -0
  42. package/dist/react.mjs.map +1 -0
  43. package/dist/sdk/auth.d.ts +25 -0
  44. package/dist/sdk/index.d.ts +155 -0
  45. package/dist/sdk/record-pipelined-steps.d.ts +19 -0
  46. package/dist/sdk.mjs +245 -0
  47. package/dist/sdk.mjs.map +1 -0
  48. package/dist/stream/processor.d.ts +383 -0
  49. package/dist/stream/processor.mjs +605 -0
  50. package/dist/stream/processor.mjs.map +1 -0
  51. package/dist/stream/run.d.ts +61 -0
  52. package/dist/stream/run.mjs +45 -0
  53. package/dist/stream/run.mjs.map +1 -0
  54. package/dist/stream/test-support.d.ts +45 -0
  55. package/dist/stream/test-support.mjs +196 -0
  56. package/dist/stream/test-support.mjs.map +1 -0
  57. package/dist/usingCtx-inzbY1Qz.mjs +57 -0
  58. package/package.json +93 -30
  59. package/bin/iterate.js +0 -86
  60. package/dist/cli-DMS4kJph.mjs +0 -868
  61. package/dist/cli-DMS4kJph.mjs.map +0 -1
  62. package/dist/config-DtnR7Lv7.mjs +0 -170
  63. package/dist/config-DtnR7Lv7.mjs.map +0 -1
  64. package/dist/index.d.mts +0 -5
  65. package/dist/index.d.mts.map +0 -1
  66. package/dist/index.mjs +0 -8
  67. package/dist/index.mjs.map +0 -1
  68. package/dist/stream-tui/agent-chat-terminal.d.mts +0 -1
  69. package/dist/stream-tui/agent-chat-terminal.mjs +0 -933
  70. package/dist/stream-tui/agent-chat-terminal.mjs.map +0 -1
  71. package/dist/worker.d.mts +0 -33
  72. package/dist/worker.mjs +0 -18
  73. package/dist/worker.mjs.map +0 -1
@@ -1,933 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { c as connectItx, r as readConfig } from "../config-DtnR7Lv7.mjs";
3
- import { StyledText, bg, createCliRenderer, fg } from "@opentui/core";
4
- import { createRoot, useKeyboard } from "@opentui/react";
5
- import { useCallback, useState, useSyncExternalStore } from "react";
6
- import { jsx, jsxs } from "@opentui/react/jsx-runtime";
7
- //#region ../ui/src/components/events/agent-ui-reducer.ts
8
- function initialAgentUiState() {
9
- return {
10
- live: null,
11
- queuedUserMessages: [],
12
- eventCount: 0,
13
- presence: [],
14
- nextLocalIndex: 0
15
- };
16
- }
17
- /**
18
- * Fold a batch of events into settled-item ops + the resulting state.
19
- * Idempotent by construction: replaying the same events from the same entry
20
- * state yields the same ops, and the processor upserts by local_index.
21
- */
22
- function planAgentUiOps(start, events) {
23
- const ops = [];
24
- let state = start;
25
- for (const event of events) state = reduceAgentUiEvent(state, event, ops);
26
- return {
27
- endState: state,
28
- ops
29
- };
30
- }
31
- /** Append a settled item op at the next dense position. */
32
- function emitItem(state, ops, item) {
33
- ops.push({
34
- localIndex: state.nextLocalIndex,
35
- item
36
- });
37
- return {
38
- ...state,
39
- nextLocalIndex: state.nextLocalIndex + 1
40
- };
41
- }
42
- const AGENT_LLM_REQUEST_REQUESTED = "events.iterate.com/agent/llm-request-requested";
43
- const AGENT_LLM_REQUEST_COMPLETED = "events.iterate.com/agent/llm-request-completed";
44
- const AGENT_LLM_REQUEST_CANCELLED = "events.iterate.com/agent/llm-request-cancelled";
45
- const AGENT_OUTPUT_ADDED = "events.iterate.com/agent/output-added";
46
- const AGENT_STATUS_UPDATED = "events.iterate.com/agent/status-updated";
47
- const OPENAI_WS_REQUEST_STARTED = "events.iterate.com/openai-ws/llm-request-started";
48
- const OPENAI_WS_RESPONSE_CHUNK = "events.iterate.com/openai-ws/llm-response-chunk";
49
- const CLOUDFLARE_AI_REQUEST_STARTED = "events.iterate.com/cloudflare-ai/llm-request-started";
50
- const CLOUDFLARE_AI_RESPONSE_CHUNK = "events.iterate.com/cloudflare-ai/llm-response-chunk";
51
- const SCRIPT_EXECUTION_REQUESTED = "events.iterate.com/capability-host/script-execution-requested";
52
- const SCRIPT_EXECUTION_COMPLETED = "events.iterate.com/capability-host/script-execution-completed";
53
- const CODEMODE_SCRIPT_EXECUTION_REQUESTED = "events.iterate.com/codemode/script-execution-requested";
54
- const CODEMODE_SCRIPT_EXECUTION_COMPLETED = "events.iterate.com/codemode/script-execution-completed";
55
- const STREAM_SUBSCRIBER_CONNECTED = "events.iterate.com/stream/subscriber-connected";
56
- const STREAM_SUBSCRIBER_DISCONNECTED = "events.iterate.com/stream/subscriber-disconnected";
57
- const STREAM_WOKEN = "events.iterate.com/stream/woken";
58
- function reduceAgentUiEvent(previous, event, ops) {
59
- const state = {
60
- ...previous,
61
- queuedUserMessages: previous.queuedUserMessages ?? [],
62
- eventCount: previous.eventCount + 1
63
- };
64
- const timestampMs = Date.parse(event.createdAt);
65
- switch (event.type) {
66
- case "events.iterate.com/agents/user-message-received": {
67
- const text = readString(event, "content");
68
- if (text == null) return state;
69
- const hasRunningStep = state.live?.steps.some((step) => step.status === "running") ?? false;
70
- const item = {
71
- kind: "user",
72
- id: `user-${event.offset}`,
73
- text,
74
- timestampMs
75
- };
76
- if (hasRunningStep) return {
77
- ...state,
78
- queuedUserMessages: [...state.queuedUserMessages, item]
79
- };
80
- return emitItem(hasRunningStep ? state : settleLive(state, timestampMs, ops), ops, item);
81
- }
82
- case "events.iterate.com/agents/web-message-sent":
83
- case "events.iterate.com/agents/tui-message-sent": {
84
- const text = readString(event, "message");
85
- if (text == null) return state;
86
- return emitItem(settleLive(state, timestampMs, ops), ops, {
87
- kind: "assistant",
88
- id: `assistant-${event.offset}`,
89
- text,
90
- timestampMs
91
- });
92
- }
93
- case AGENT_LLM_REQUEST_REQUESTED: {
94
- const base = state.queuedUserMessages.length === 0 ? state : flushQueuedUserMessages(settleLive(state, timestampMs, ops), ops);
95
- const live = ensureLive(base, event.offset, timestampMs);
96
- const model = readString(event, "model");
97
- const step = {
98
- kind: "llm",
99
- id: `llm-${event.offset}`,
100
- llmRequestId: event.offset,
101
- status: "running",
102
- ...model == null ? {} : { model },
103
- thinkingText: "",
104
- responseText: "",
105
- startedAtMs: timestampMs
106
- };
107
- return {
108
- ...base,
109
- live: {
110
- ...live,
111
- steps: [...live.steps, step]
112
- }
113
- };
114
- }
115
- case OPENAI_WS_REQUEST_STARTED:
116
- case CLOUDFLARE_AI_REQUEST_STARTED: {
117
- const llmRequestId = readNumber(event, "llmRequestId");
118
- const model = readString(event, "model");
119
- if (llmRequestId == null || model == null) return state;
120
- return updateLlmStep(state, llmRequestId, (step) => ({
121
- ...step,
122
- model
123
- }));
124
- }
125
- case OPENAI_WS_RESPONSE_CHUNK: {
126
- const llmRequestId = readNumber(event, "llmRequestId");
127
- const message = readRecord(event, "chunk");
128
- if (llmRequestId == null || message == null) return state;
129
- const frameType = typeof message.type === "string" ? message.type : "";
130
- const delta = typeof message.delta === "string" ? message.delta : "";
131
- if (frameType === "response.output_text.delta" && delta !== "") return updateLlmStep(state, llmRequestId, (step) => ({
132
- ...step,
133
- responseText: step.status === "running" ? step.responseText + delta : step.responseText
134
- }));
135
- if ((frameType === "response.reasoning_summary_text.delta" || frameType === "response.reasoning_text.delta") && delta !== "") return updateLlmStep(state, llmRequestId, (step) => ({
136
- ...step,
137
- thinkingText: step.status === "running" ? step.thinkingText + delta : step.thinkingText
138
- }));
139
- if (frameType === "response.reasoning_summary_part.added") return updateLlmStep(state, llmRequestId, (step) => ({
140
- ...step,
141
- thinkingText: step.status !== "running" || step.thinkingText === "" ? step.thinkingText : `${step.thinkingText}\n\n`
142
- }));
143
- return state;
144
- }
145
- case CLOUDFLARE_AI_RESPONSE_CHUNK: {
146
- const llmRequestId = readNumber(event, "llmRequestId");
147
- const chunk = readPayloadRecord(event)?.chunk;
148
- if (llmRequestId == null) return state;
149
- const { responseDelta, thinkingDelta } = extractCloudflareChunkDeltas(chunk);
150
- if (responseDelta === "" && thinkingDelta === "") return state;
151
- return updateLlmStep(state, llmRequestId, (step) => ({
152
- ...step,
153
- responseText: step.status === "running" ? step.responseText + responseDelta : step.responseText,
154
- thinkingText: step.status === "running" ? step.thinkingText + thinkingDelta : step.thinkingText
155
- }));
156
- }
157
- case AGENT_OUTPUT_ADDED: {
158
- const llmRequestId = readNumber(event, "llmRequestId");
159
- const content = readString(event, "content");
160
- if (llmRequestId == null || content == null) return state;
161
- return updateLlmStep(state, llmRequestId, (step) => step.status === "running" ? {
162
- ...step,
163
- responseText: content
164
- } : step);
165
- }
166
- case AGENT_LLM_REQUEST_COMPLETED: {
167
- const llmRequestId = readNumber(event, "llmRequestId");
168
- if (llmRequestId == null) return state;
169
- const payload = readPayloadRecord(event);
170
- const result = isRecord(payload?.result) ? payload.result : void 0;
171
- const status = typeof result?.status === "string" ? result.status : "success";
172
- const usage = readUsageTokens(result?.usage);
173
- const errorMessage = isRecord(result?.error) && typeof result.error.message === "string" ? result.error.message : void 0;
174
- return updateLlmStep(state, llmRequestId, (step) => step.outcome === "cancelled" ? step : {
175
- ...step,
176
- status: "done",
177
- outcome: status === "success" ? "completed" : "failed",
178
- ...typeof payload?.provider === "string" ? { provider: payload.provider } : {},
179
- ...typeof payload?.durationMs === "number" ? { durationMs: payload.durationMs } : {},
180
- ...usage.input == null ? {} : { inputTokens: usage.input },
181
- ...usage.output == null ? {} : { outputTokens: usage.output },
182
- ...errorMessage == null ? {} : { errorMessage },
183
- ...typeof result?.providerResponseId === "string" ? { providerResponseId: result.providerResponseId } : {}
184
- });
185
- }
186
- case AGENT_LLM_REQUEST_CANCELLED: {
187
- const llmRequestId = readNumber(event, "llmRequestId");
188
- if (llmRequestId == null) return state;
189
- return updateLlmStep(state, llmRequestId, (step) => ({
190
- ...step,
191
- status: "done",
192
- outcome: "cancelled"
193
- }));
194
- }
195
- case SCRIPT_EXECUTION_REQUESTED:
196
- case CODEMODE_SCRIPT_EXECUTION_REQUESTED: {
197
- const payload = readPayloadRecord(event);
198
- const executionId = typeof payload?.executionId === "string" ? payload.executionId : `exec-${event.offset}`;
199
- const live = ensureLive(state, event.offset, timestampMs);
200
- const step = {
201
- kind: "code",
202
- id: `code-${executionId}`,
203
- executionId,
204
- status: "running",
205
- code: typeof payload?.code === "string" ? payload.code : "",
206
- startedAtMs: timestampMs
207
- };
208
- return {
209
- ...state,
210
- live: {
211
- ...live,
212
- steps: [...live.steps, step]
213
- }
214
- };
215
- }
216
- case SCRIPT_EXECUTION_COMPLETED:
217
- case CODEMODE_SCRIPT_EXECUTION_COMPLETED: {
218
- const payload = readPayloadRecord(event);
219
- if (payload == null || state.live == null) return state;
220
- const executionId = typeof payload.executionId === "string" ? payload.executionId : null;
221
- const outcome = readCodeOutcome(payload);
222
- const steps = [...state.live.steps];
223
- const index = executionId == null ? steps.findLastIndex((step) => step.kind === "code" && step.status === "running") : steps.findIndex((step) => step.kind === "code" && step.executionId === executionId);
224
- const step = steps[index];
225
- if (step == null || step.kind !== "code") return state;
226
- steps[index] = {
227
- ...step,
228
- status: "done",
229
- ...outcome
230
- };
231
- return {
232
- ...state,
233
- live: {
234
- ...state.live,
235
- steps
236
- }
237
- };
238
- }
239
- case AGENT_STATUS_UPDATED:
240
- if (readString(event, "status") !== "idle") return state;
241
- if (state.live == null) return state;
242
- return {
243
- ...state,
244
- live: {
245
- ...state.live,
246
- status: "waiting"
247
- }
248
- };
249
- case STREAM_SUBSCRIBER_CONNECTED: {
250
- const payload = readPayloadRecord(event);
251
- if (payload == null) return state;
252
- const subscriptionKey = typeof payload.subscriptionKey === "string" ? payload.subscriptionKey : null;
253
- if (subscriptionKey == null) return state;
254
- const direction = payload.direction === "inbound" ? "inbound" : "outbound";
255
- const subscriber = isRecord(payload.subscriber) ? payload.subscriber : void 0;
256
- const announcement = readProcessorAnnouncement(subscriber?.processor);
257
- const entry = {
258
- subscriptionKey,
259
- direction,
260
- connected: true,
261
- ...typeof subscriber?.description === "string" ? { description: subscriber.description } : {},
262
- ...announcement == null ? {} : { processor: announcement }
263
- };
264
- const existingIndex = state.presence.findIndex((candidate) => candidate.subscriptionKey === subscriptionKey);
265
- const presence = existingIndex === -1 ? [...state.presence, entry] : state.presence.map((candidate, index) => index === existingIndex ? {
266
- ...candidate,
267
- ...entry
268
- } : candidate);
269
- return {
270
- ...state,
271
- presence
272
- };
273
- }
274
- case STREAM_SUBSCRIBER_DISCONNECTED: {
275
- const subscriptionKey = readString(event, "subscriptionKey");
276
- if (subscriptionKey == null) return state;
277
- return {
278
- ...state,
279
- presence: state.presence.map((entry) => entry.subscriptionKey === subscriptionKey ? {
280
- ...entry,
281
- connected: false
282
- } : entry)
283
- };
284
- }
285
- case STREAM_WOKEN: return {
286
- ...state,
287
- presence: state.presence.map((entry) => entry.connected ? {
288
- ...entry,
289
- connected: false
290
- } : entry)
291
- };
292
- default: return state;
293
- }
294
- }
295
- function ensureLive(state, offset, startedAtMs) {
296
- if (state.live != null) return {
297
- ...state.live,
298
- status: "running"
299
- };
300
- return {
301
- kind: "activity",
302
- id: `activity-${offset}`,
303
- status: "running",
304
- steps: [],
305
- startedAtMs
306
- };
307
- }
308
- /** Closes the live activity (if any) and emits it as a settled item. */
309
- function settleLive(state, endedAtMs, ops) {
310
- if (state.live == null) return state;
311
- if (state.live.steps.length === 0) return {
312
- ...state,
313
- live: null
314
- };
315
- const settled = {
316
- ...state.live,
317
- status: "done",
318
- endedAtMs,
319
- steps: state.live.steps.map((step) => step.status === "running" ? {
320
- ...step,
321
- status: "done"
322
- } : step)
323
- };
324
- return emitItem({
325
- ...state,
326
- live: null
327
- }, ops, settled);
328
- }
329
- function flushQueuedUserMessages(state, ops) {
330
- let next = {
331
- ...state,
332
- queuedUserMessages: []
333
- };
334
- for (const item of state.queuedUserMessages) next = emitItem(next, ops, item);
335
- return next;
336
- }
337
- function updateLlmStep(state, llmRequestId, update) {
338
- if (state.live == null) return state;
339
- const index = state.live.steps.findIndex((step) => step.kind === "llm" && step.llmRequestId === llmRequestId);
340
- const step = state.live.steps[index];
341
- if (step == null || step.kind !== "llm") return state;
342
- const steps = [...state.live.steps];
343
- steps[index] = update(step);
344
- return {
345
- ...state,
346
- live: {
347
- ...state.live,
348
- steps
349
- }
350
- };
351
- }
352
- function extractCloudflareChunkDeltas(chunk) {
353
- if (typeof chunk === "string") return {
354
- responseDelta: chunk,
355
- thinkingDelta: ""
356
- };
357
- if (!isRecord(chunk)) return {
358
- responseDelta: "",
359
- thinkingDelta: ""
360
- };
361
- if (typeof chunk.response === "string") return {
362
- responseDelta: chunk.response,
363
- thinkingDelta: ""
364
- };
365
- if (Array.isArray(chunk.choices) && isRecord(chunk.choices[0])) {
366
- const delta = isRecord(chunk.choices[0].delta) ? chunk.choices[0].delta : void 0;
367
- return {
368
- responseDelta: typeof delta?.content === "string" ? delta.content : "",
369
- thinkingDelta: typeof delta?.reasoning_content === "string" ? delta.reasoning_content : ""
370
- };
371
- }
372
- if (isRecord(chunk.delta)) return {
373
- responseDelta: typeof chunk.delta.text === "string" ? chunk.delta.text : "",
374
- thinkingDelta: typeof chunk.delta.thinking === "string" ? chunk.delta.thinking : ""
375
- };
376
- return {
377
- responseDelta: "",
378
- thinkingDelta: ""
379
- };
380
- }
381
- function readCodeOutcome(payload) {
382
- const outcome = isRecord(payload.outcome) ? payload.outcome : void 0;
383
- const success = payload.ok !== false && payload.error == null && outcome?.status !== "threw";
384
- const result = "result" in payload ? payload.result : outcome?.value;
385
- const error = typeof payload.error === "string" ? payload.error : outcome?.status === "threw" ? stringifyUnknown(outcome.error) : void 0;
386
- const logs = Array.isArray(payload.logs) ? payload.logs.filter((line) => typeof line === "string") : void 0;
387
- return {
388
- success,
389
- ...result === void 0 ? {} : { result },
390
- ...error == null ? {} : { errorMessage: error },
391
- ...logs == null ? {} : { logs },
392
- ...typeof payload.durationMs === "number" ? { durationMs: payload.durationMs } : {}
393
- };
394
- }
395
- function readUsageTokens(usage) {
396
- if (!isRecord(usage)) return {};
397
- const input = usage.input_tokens ?? usage.prompt_tokens;
398
- const output = usage.output_tokens ?? usage.completion_tokens;
399
- return {
400
- ...typeof input === "number" ? { input } : {},
401
- ...typeof output === "number" ? { output } : {}
402
- };
403
- }
404
- function readProcessorAnnouncement(value) {
405
- if (!isRecord(value)) return null;
406
- const announcement = isRecord(value.announcement) ? value.announcement : value;
407
- if (typeof announcement.slug !== "string" || typeof announcement.version !== "string") return null;
408
- return {
409
- slug: announcement.slug,
410
- version: announcement.version,
411
- description: typeof announcement.description === "string" ? announcement.description : "",
412
- consumes: readStringArray(announcement.consumes),
413
- emits: readStringArray(announcement.emits),
414
- ownedEvents: Array.isArray(announcement.ownedEvents) ? announcement.ownedEvents.filter((owned) => isRecord(owned)).filter((owned) => typeof owned.type === "string").map((owned) => ({
415
- type: owned.type,
416
- ...typeof owned.description === "string" ? { description: owned.description } : {}
417
- })) : []
418
- };
419
- }
420
- function readStringArray(value) {
421
- return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
422
- }
423
- function readString(event, key) {
424
- const value = readPayloadRecord(event)?.[key];
425
- return typeof value === "string" ? value : null;
426
- }
427
- function readNumber(event, key) {
428
- const value = readPayloadRecord(event)?.[key];
429
- return typeof value === "number" && Number.isFinite(value) ? value : null;
430
- }
431
- function readRecord(event, key) {
432
- const value = readPayloadRecord(event)?.[key];
433
- return isRecord(value) ? value : null;
434
- }
435
- function readPayloadRecord(event) {
436
- return isRecord(event.payload) ? event.payload : null;
437
- }
438
- function isRecord(value) {
439
- return value != null && typeof value === "object" && !Array.isArray(value);
440
- }
441
- function stringifyUnknown(value) {
442
- if (typeof value === "string") return value;
443
- try {
444
- return JSON.stringify(value);
445
- } catch {
446
- return String(value);
447
- }
448
- }
449
- //#endregion
450
- //#region src/stream-tui/agent-feed-model.ts
451
- /**
452
- * In-memory agent conversation feed for the chat TUI.
453
- *
454
- * This is the node-side sibling of the browser mirror's agent-ui processor
455
- * (apps/os/src/domains/streams/client-libraries/processors/agent-ui-processor.ts):
456
- * both fold the SAME shared reducer (`planAgentUiOps` from @iterate-com/ui)
457
- * over agent stream events. The browser persists settled items into SQLite for
458
- * a virtual list; a terminal session is ephemeral and small, so this model
459
- * keeps the settled items in a plain array and the streaming live activity in
460
- * the reduced state — no database, no processor host.
461
- */
462
- function createAgentFeedModel() {
463
- let state = initialAgentUiState();
464
- let items = [];
465
- let lastOffset = 0;
466
- let snapshot = {
467
- items,
468
- live: null,
469
- eventCount: 0,
470
- lastOffset
471
- };
472
- return {
473
- snapshot: () => snapshot,
474
- applyEvents(events) {
475
- const fresh = events.filter((event) => event.offset > lastOffset);
476
- if (fresh.length === 0) return false;
477
- lastOffset = fresh[fresh.length - 1].offset;
478
- const { endState, ops } = planAgentUiOps(state, fresh);
479
- if (ops.length > 0) {
480
- items = [...items];
481
- for (const op of ops) items[op.localIndex] = op.item;
482
- }
483
- state = endState;
484
- snapshot = {
485
- items,
486
- live: state.live,
487
- eventCount: state.eventCount,
488
- lastOffset
489
- };
490
- return true;
491
- }
492
- };
493
- }
494
- //#endregion
495
- //#region src/stream-tui/agent-connection.ts
496
- const RECONNECT_DELAY_MS = 1e3;
497
- const MAX_RECONNECT_DELAY_MS = 15e3;
498
- /**
499
- * Resolve itx credentials for the TUI, in priority order: an admin API secret
500
- * from the environment (doppler / e2e lanes), an explicit bearer token, then
501
- * the stored `iterate login` session for the named config. The launcher
502
- * (`iterate chat`) refreshes the stored session before spawning the TUI, so a
503
- * plain bearer read is enough here.
504
- */
505
- function resolveItxAuth(input) {
506
- const adminSecret = readEnv("APP_CONFIG_ADMIN_API_SECRET");
507
- if (adminSecret) return {
508
- type: "admin-secret",
509
- secret: adminSecret
510
- };
511
- const bearerToken = readEnv("ITERATE_BEARER_TOKEN");
512
- if (bearerToken) return {
513
- type: "bearer",
514
- token: bearerToken
515
- };
516
- if (input.configName) {
517
- const config = readConfig(input.configName, { throw: true });
518
- if (config.session?.token) return {
519
- type: "bearer",
520
- token: config.session.token
521
- };
522
- }
523
- throw new Error("No credentials: run `iterate login`, or set an admin API secret (APP_CONFIG_ADMIN_API_SECRET) or a bearer token (ITERATE_BEARER_TOKEN).");
524
- }
525
- function connectAgentFeed(input) {
526
- let disposed = false;
527
- let agent;
528
- let subscription;
529
- let consecutiveFailures = 0;
530
- const disposeAgent = () => {
531
- try {
532
- subscription?.[Symbol.dispose]?.();
533
- agent?.[Symbol.dispose]?.();
534
- } catch {}
535
- subscription = void 0;
536
- agent = void 0;
537
- };
538
- const scheduleReconnect = (detail) => {
539
- if (disposed) return;
540
- disposeAgent();
541
- consecutiveFailures += 1;
542
- const delay = Math.min(RECONNECT_DELAY_MS * consecutiveFailures, MAX_RECONNECT_DELAY_MS);
543
- input.onStatus({
544
- kind: "reconnecting",
545
- detail
546
- });
547
- setTimeout(() => void establish(), delay);
548
- };
549
- async function establish() {
550
- if (disposed) return;
551
- input.onStatus({ kind: "connecting" });
552
- const nextAgent = connectItx({
553
- auth: input.auth,
554
- baseUrl: input.baseUrl,
555
- projectId: input.projectId,
556
- agentPath: input.agentPath
557
- });
558
- agent = nextAgent;
559
- nextAgent.onRpcBroken?.((error) => {
560
- if (agent !== nextAgent) return;
561
- scheduleReconnect(errorMessage(error));
562
- });
563
- try {
564
- subscription = await nextAgent.stream.subscribe({
565
- processEventBatch: (batch) => input.onEvents(batch.events),
566
- replayAfterOffset: input.replayAfterOffset(),
567
- subscriber: { description: "iterate chat TUI" }
568
- });
569
- if (disposed) {
570
- disposeAgent();
571
- return;
572
- }
573
- consecutiveFailures = 0;
574
- input.onStatus({ kind: "live" });
575
- } catch (error) {
576
- if (agent === nextAgent) scheduleReconnect(errorMessage(error));
577
- }
578
- }
579
- establish();
580
- return {
581
- async sendMessage(text) {
582
- if (agent === void 0) throw new Error("not connected");
583
- await agent.sendMessage(text);
584
- },
585
- dispose() {
586
- disposed = true;
587
- disposeAgent();
588
- }
589
- };
590
- }
591
- function errorMessage(error) {
592
- return error instanceof Error ? error.message : String(error);
593
- }
594
- function readEnv(name) {
595
- const value = process.env[name]?.trim();
596
- return value ? value : void 0;
597
- }
598
- //#endregion
599
- //#region src/stream-tui/feed-format.ts
600
- function formatActivitySummary(activity) {
601
- const codeCount = activity.steps.filter((step) => step.kind === "code").length;
602
- const requestCount = activity.steps.filter((step) => step.kind === "llm").length;
603
- const interrupted = activity.steps.some((step) => step.kind === "llm" && step.outcome === "cancelled");
604
- const parts = [];
605
- if (codeCount > 0) parts.push(`Ran code ${codeCount}×`);
606
- parts.push(`${requestCount} request${requestCount === 1 ? "" : "s"}`);
607
- if (interrupted) parts.push("interrupted");
608
- const totalMs = activity.endedAtMs == null ? null : Math.max(0, activity.endedAtMs - activity.startedAtMs);
609
- if (totalMs != null && totalMs > 0) parts.push(formatSeconds(totalMs));
610
- return parts.join(" · ");
611
- }
612
- function formatStepLine(step) {
613
- const parts = [step.kind === "code" ? "Ran code" : step.model ?? step.provider ?? "LLM request"];
614
- if (step.kind === "llm") {
615
- if (step.inputTokens != null || step.outputTokens != null) parts.push(`${formatTokens(step.inputTokens)} → ${formatTokens(step.outputTokens)} tok`);
616
- if (step.outcome === "failed") parts.push("failed");
617
- if (step.outcome === "cancelled") parts.push("interrupted");
618
- } else if (step.success === false) parts.push("failed");
619
- if (step.durationMs != null) parts.push(formatSeconds(step.durationMs));
620
- if (step.status === "running") parts.push("running");
621
- return parts.join(" · ");
622
- }
623
- function formatSeconds(ms) {
624
- return `${(ms / 1e3).toFixed(1)}s`;
625
- }
626
- function formatTokens(count) {
627
- if (count == null) return "?";
628
- if (count < 1e3) return String(count);
629
- return `${(count / 1e3).toFixed(1)}k`;
630
- }
631
- /** The last `maxChars` of streamed text, trimmed to whole lines where possible. */
632
- function streamingTail(text, maxChars = 600) {
633
- const trimmed = text.trimEnd();
634
- if (trimmed.length <= maxChars) return trimmed;
635
- const tail = trimmed.slice(-maxChars);
636
- const firstNewline = tail.indexOf("\n");
637
- return `…${firstNewline === -1 ? tail : tail.slice(firstNewline)}`;
638
- }
639
- //#endregion
640
- //#region src/stream-tui/agent-chat-terminal.tsx
641
- /** @jsxImportSource @opentui/react */
642
- /**
643
- * React/OpenTUI terminal chat with one project agent.
644
- *
645
- * The data layer is the shared client stack, not a bespoke stream client:
646
- * `connectItx` (apps/os/src/itx-client.ts) hands us the same `Agent`
647
- * capability the web app uses, a live `stream.subscribe` pumps events into
648
- * the shared agent-ui reducer (@iterate-com/ui), and sends go through
649
- * `agent.sendMessage`. This file owns only terminal runtime state and
650
- * rendering.
651
- */
652
- if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("iterate chat requires an interactive terminal.");
653
- const COLORS = {
654
- bg: "#0b0f14",
655
- surface: "#27272a",
656
- border: "#3f3f46",
657
- accent: "#22c55e",
658
- warning: "#facc15",
659
- danger: "#ef4444",
660
- text: "#e5e7eb",
661
- textSecondary: "#9ca3af",
662
- textBody: "#d1d5db",
663
- textMuted: "#6b7280",
664
- agent: "#a78bfa"
665
- };
666
- const args = parseArgs(process.argv.slice(2));
667
- const model = createAgentFeedModel();
668
- let appState = {
669
- feed: model.snapshot(),
670
- status: { kind: "connecting" },
671
- notice: ""
672
- };
673
- const listeners = /* @__PURE__ */ new Set();
674
- function patchAppState(patch) {
675
- appState = {
676
- ...appState,
677
- ...patch
678
- };
679
- for (const listener of listeners) listener();
680
- }
681
- const connection = connectAgentFeed({
682
- auth: resolveItxAuth({ configName: process.env.ITERATE_CONFIG_NAME }),
683
- baseUrl: args.baseUrl,
684
- projectId: args.projectId,
685
- agentPath: args.agentPath,
686
- replayAfterOffset: () => model.snapshot().lastOffset,
687
- onEvents: (events) => {
688
- if (model.applyEvents(events)) patchAppState({ feed: model.snapshot() });
689
- },
690
- onStatus: (status) => patchAppState({ status })
691
- });
692
- function AgentChatApp() {
693
- const state = useSyncExternalStore(useCallback((listener) => {
694
- listeners.add(listener);
695
- return () => listeners.delete(listener);
696
- }, []), () => appState);
697
- const [composerValue, setComposerValue] = useState("");
698
- const [composerRevision, setComposerRevision] = useState(0);
699
- const clearComposer = useCallback(() => {
700
- setComposerValue("");
701
- setComposerRevision((previous) => previous + 1);
702
- }, []);
703
- useKeyboard((key) => {
704
- if (key.name === "escape") clearComposer();
705
- });
706
- const submit = useCallback((value) => {
707
- const message = value.trim();
708
- if (message === "") return;
709
- clearComposer();
710
- patchAppState({ notice: "sending…" });
711
- connection.sendMessage(message).then(() => patchAppState({ notice: "" })).catch((error) => {
712
- patchAppState({ notice: `send failed: ${error instanceof Error ? error.message : String(error)}` });
713
- });
714
- }, [clearComposer]);
715
- return /* @__PURE__ */ jsxs("box", {
716
- width: "100%",
717
- height: "100%",
718
- flexDirection: "column",
719
- backgroundColor: COLORS.bg,
720
- children: [
721
- /* @__PURE__ */ jsx(ChatHeader, {
722
- status: state.status,
723
- notice: state.notice,
724
- eventCount: state.feed.eventCount
725
- }),
726
- /* @__PURE__ */ jsxs("scrollbox", {
727
- width: "100%",
728
- flexGrow: 1,
729
- border: true,
730
- borderStyle: "single",
731
- borderColor: COLORS.border,
732
- backgroundColor: COLORS.bg,
733
- stickyScroll: true,
734
- stickyStart: "bottom",
735
- contentOptions: {
736
- flexDirection: "column",
737
- paddingLeft: 1,
738
- paddingRight: 1,
739
- gap: 1
740
- },
741
- children: [
742
- state.feed.items.length === 0 && state.feed.live == null ? /* @__PURE__ */ jsxs("text", {
743
- fg: COLORS.textMuted,
744
- children: [
745
- "No messages yet — say something to ",
746
- args.agentPath.slice(8),
747
- "."
748
- ]
749
- }) : null,
750
- state.feed.items.map((item) => /* @__PURE__ */ jsx(FeedItem, { item }, item.id)),
751
- state.feed.live == null ? null : /* @__PURE__ */ jsx(LiveActivity, { activity: state.feed.live })
752
- ]
753
- }),
754
- /* @__PURE__ */ jsx("box", {
755
- width: "100%",
756
- height: 3,
757
- border: true,
758
- borderStyle: "single",
759
- borderColor: COLORS.accent,
760
- backgroundColor: COLORS.bg,
761
- paddingLeft: 1,
762
- paddingRight: 1,
763
- children: /* @__PURE__ */ jsx("input", {
764
- width: "100%",
765
- value: composerValue,
766
- placeholder: "Message the agent (Enter to send, Ctrl+C to quit)",
767
- focused: true,
768
- backgroundColor: "transparent",
769
- focusedBackgroundColor: "transparent",
770
- textColor: COLORS.text,
771
- focusedTextColor: COLORS.text,
772
- placeholderColor: COLORS.textMuted,
773
- cursorColor: COLORS.accent,
774
- onInput: setComposerValue,
775
- onSubmit: (value) => {
776
- if (typeof value === "string") submit(value);
777
- }
778
- }, composerRevision)
779
- })
780
- ]
781
- });
782
- }
783
- function ChatHeader(props) {
784
- const statusLabel = props.status.kind === "live" ? "live" : props.status.kind === "connecting" ? "connecting" : `reconnecting (${props.status.detail})`;
785
- const statusColor = props.status.kind === "live" ? COLORS.accent : props.status.kind === "connecting" ? COLORS.warning : COLORS.danger;
786
- const meta = [
787
- `${props.eventCount} event${props.eventCount === 1 ? "" : "s"}`,
788
- statusLabel,
789
- props.notice
790
- ].filter(Boolean).join(" · ");
791
- return /* @__PURE__ */ jsxs("box", {
792
- width: "100%",
793
- height: 3,
794
- border: true,
795
- borderStyle: "single",
796
- borderColor: COLORS.border,
797
- backgroundColor: COLORS.surface,
798
- flexDirection: "row",
799
- paddingLeft: 1,
800
- paddingRight: 1,
801
- gap: 1,
802
- children: [
803
- /* @__PURE__ */ jsx("text", {
804
- width: 6,
805
- content: getBrandMarkText()
806
- }),
807
- /* @__PURE__ */ jsx("text", {
808
- flexGrow: 1,
809
- fg: COLORS.text,
810
- content: `${args.projectId} ${args.agentPath}`
811
- }),
812
- /* @__PURE__ */ jsx("text", {
813
- fg: COLORS.textSecondary,
814
- content: meta
815
- }),
816
- /* @__PURE__ */ jsx("text", {
817
- width: 2,
818
- fg: statusColor,
819
- children: "●"
820
- })
821
- ]
822
- });
823
- }
824
- function FeedItem(props) {
825
- if (props.item.kind === "activity") return /* @__PURE__ */ jsx(SettledActivity, { activity: props.item });
826
- return /* @__PURE__ */ jsx(Message, { item: props.item });
827
- }
828
- function Message(props) {
829
- const isUser = props.item.kind === "user";
830
- return /* @__PURE__ */ jsxs("box", {
831
- flexDirection: "column",
832
- children: [/* @__PURE__ */ jsxs("text", {
833
- fg: isUser ? COLORS.accent : COLORS.agent,
834
- children: [isUser ? "you ›" : "agent ›", /* @__PURE__ */ jsxs("span", {
835
- fg: COLORS.textMuted,
836
- children: [" ", formatClock(props.item.timestampMs)]
837
- })]
838
- }), /* @__PURE__ */ jsx("text", {
839
- fg: COLORS.textBody,
840
- children: props.item.text
841
- })]
842
- });
843
- }
844
- function SettledActivity(props) {
845
- return /* @__PURE__ */ jsxs("box", {
846
- flexDirection: "column",
847
- children: [/* @__PURE__ */ jsxs("text", {
848
- fg: COLORS.textMuted,
849
- children: ["✦ ", formatActivitySummary(props.activity)]
850
- }), props.activity.steps.map((step) => /* @__PURE__ */ jsxs("text", {
851
- fg: COLORS.textMuted,
852
- children: [
853
- " ",
854
- "· ",
855
- formatStepLine(step)
856
- ]
857
- }, step.id))]
858
- });
859
- }
860
- function LiveActivity(props) {
861
- const lastStep = props.activity.steps[props.activity.steps.length - 1];
862
- const thinking = lastStep?.kind === "llm" ? streamingTail(lastStep.thinkingText) : "";
863
- const streamed = lastStep?.kind === "llm" ? streamingTail(lastStep.responseText) : lastStep?.kind === "code" ? streamingTail(lastStep.code) : "";
864
- return /* @__PURE__ */ jsxs("box", {
865
- flexDirection: "column",
866
- children: [
867
- /* @__PURE__ */ jsxs("text", {
868
- fg: props.activity.status === "waiting" ? COLORS.textMuted : COLORS.warning,
869
- children: ["✦ ", props.activity.status === "waiting" ? "waiting" : "working…"]
870
- }),
871
- props.activity.steps.map((step) => /* @__PURE__ */ jsxs("text", {
872
- fg: COLORS.textMuted,
873
- children: [
874
- " ",
875
- "· ",
876
- formatStepLine(step)
877
- ]
878
- }, step.id)),
879
- thinking === "" ? null : /* @__PURE__ */ jsx("text", {
880
- fg: COLORS.textMuted,
881
- children: thinking
882
- }),
883
- streamed === "" ? null : /* @__PURE__ */ jsx("text", {
884
- fg: COLORS.textSecondary,
885
- children: streamed
886
- })
887
- ]
888
- });
889
- }
890
- function getBrandMarkText() {
891
- return new StyledText([
892
- fg("#000000")(bg(COLORS.surface)("▐")),
893
- bg("#000000")(fg("#ffffff")(" 𝑖 ")),
894
- fg("#000000")(bg(COLORS.surface)("▌"))
895
- ]);
896
- }
897
- function formatClock(timestampMs) {
898
- return new Date(timestampMs).toLocaleTimeString(void 0, {
899
- hour: "2-digit",
900
- minute: "2-digit"
901
- });
902
- }
903
- function parseArgs(argv) {
904
- const baseUrl = readFlag(argv, "--base-url");
905
- const projectId = readFlag(argv, "--project-id");
906
- const agentPath = readFlag(argv, "--agent-path");
907
- if (baseUrl == null || projectId == null || agentPath == null) throw new Error("Usage: bun agent-chat-terminal.tsx --base-url <url> --project-id <prj_id> --agent-path </agents/name>");
908
- if (!agentPath.startsWith("/agents/")) throw new Error(`--agent-path must start with "/agents/", got "${agentPath}".`);
909
- return {
910
- baseUrl: baseUrl.replace(/\/+$/, ""),
911
- projectId,
912
- agentPath
913
- };
914
- }
915
- function readFlag(argv, flagName) {
916
- const index = argv.indexOf(flagName);
917
- if (index === -1) return void 0;
918
- const value = argv[index + 1];
919
- if (value == null || value.startsWith("--")) throw new Error(`${flagName} requires a value.`);
920
- return value;
921
- }
922
- const renderer = await createCliRenderer({
923
- exitOnCtrlC: true,
924
- targetFps: 30,
925
- screenMode: "alternate-screen",
926
- consoleMode: "disabled"
927
- });
928
- process.on("exit", () => connection.dispose());
929
- createRoot(renderer).render(/* @__PURE__ */ jsx(AgentChatApp, {}));
930
- //#endregion
931
- export {};
932
-
933
- //# sourceMappingURL=agent-chat-terminal.mjs.map