@kelloggt/dsh-program-agents 0.0.0-stage → 0.1.1

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 (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -2
  3. package/cordis.patch.yml +5 -0
  4. package/dist/app/agents.d.ts +50 -0
  5. package/dist/app/agents.js +70 -0
  6. package/dist/app/agents.js.map +1 -0
  7. package/dist/app/conversations.d.ts +8 -0
  8. package/dist/app/conversations.js +26 -0
  9. package/dist/app/conversations.js.map +1 -0
  10. package/dist/app/endpoints.d.ts +36 -0
  11. package/dist/app/endpoints.js +162 -0
  12. package/dist/app/endpoints.js.map +1 -0
  13. package/dist/app/inbox.d.ts +61 -0
  14. package/dist/app/inbox.js +253 -0
  15. package/dist/app/inbox.js.map +1 -0
  16. package/dist/app/index.d.ts +70 -0
  17. package/dist/app/index.js +147 -0
  18. package/dist/app/index.js.map +1 -0
  19. package/dist/app/mailbox.d.ts +63 -0
  20. package/dist/app/mailbox.js +173 -0
  21. package/dist/app/mailbox.js.map +1 -0
  22. package/dist/app/models.d.ts +32 -0
  23. package/dist/app/models.js +13 -0
  24. package/dist/app/models.js.map +1 -0
  25. package/dist/app/server.d.ts +36 -0
  26. package/dist/app/server.js +233 -0
  27. package/dist/app/server.js.map +1 -0
  28. package/dist/app/stream-server.d.ts +9 -0
  29. package/dist/app/stream-server.js +77 -0
  30. package/dist/app/stream-server.js.map +1 -0
  31. package/dist/app/stream.d.ts +4 -0
  32. package/dist/app/stream.js +93 -0
  33. package/dist/app/stream.js.map +1 -0
  34. package/dist/app/tools.d.ts +16 -0
  35. package/dist/app/tools.js +24 -0
  36. package/dist/app/tools.js.map +1 -0
  37. package/dist/client/controller.d.ts +21 -0
  38. package/dist/client/controller.js +140 -0
  39. package/dist/client/controller.js.map +1 -0
  40. package/dist/client/index.d.ts +22 -0
  41. package/dist/client/index.js +14 -0
  42. package/dist/client/index.js.map +1 -0
  43. package/dist/client/model.d.ts +66 -0
  44. package/dist/client/model.js +147 -0
  45. package/dist/client/model.js.map +1 -0
  46. package/dist/client/models.d.ts +11 -0
  47. package/dist/client/models.js +27 -0
  48. package/dist/client/models.js.map +1 -0
  49. package/dist/client/transport.d.ts +64 -0
  50. package/dist/client/transport.js +141 -0
  51. package/dist/client/transport.js.map +1 -0
  52. package/dist/client/view.d.ts +19 -0
  53. package/dist/client/view.js +69 -0
  54. package/dist/client/view.js.map +1 -0
  55. package/dist/client.js +673 -0
  56. package/dist/conversations.d.ts +92 -0
  57. package/dist/conversations.js +6 -0
  58. package/dist/conversations.js.map +1 -0
  59. package/dist/endpoints.d.ts +48 -0
  60. package/dist/endpoints.js +43 -0
  61. package/dist/endpoints.js.map +1 -0
  62. package/dist/examples/dashboard.js +42 -0
  63. package/dist/examples/dynamic-server.js +27 -0
  64. package/dist/examples/inbox-worker.js +22 -0
  65. package/dist/examples/model-slots.js +19 -0
  66. package/dist/examples/standalone-supervisor.js +20 -0
  67. package/dist/host/adapter.d.ts +195 -0
  68. package/dist/host/adapter.js +630 -0
  69. package/dist/host/adapter.js.map +1 -0
  70. package/dist/host/agents.d.ts +35 -0
  71. package/dist/host/agents.js +222 -0
  72. package/dist/host/agents.js.map +1 -0
  73. package/dist/host/audit.d.ts +14 -0
  74. package/dist/host/audit.js +39 -0
  75. package/dist/host/audit.js.map +1 -0
  76. package/dist/host/catalog.d.ts +53 -0
  77. package/dist/host/catalog.js +116 -0
  78. package/dist/host/catalog.js.map +1 -0
  79. package/dist/host/contracts.d.ts +38 -0
  80. package/dist/host/contracts.js +2 -0
  81. package/dist/host/contracts.js.map +1 -0
  82. package/dist/host/conversations.d.ts +41 -0
  83. package/dist/host/conversations.js +324 -0
  84. package/dist/host/conversations.js.map +1 -0
  85. package/dist/host/events.d.ts +12 -0
  86. package/dist/host/events.js +106 -0
  87. package/dist/host/events.js.map +1 -0
  88. package/dist/host/http.d.ts +14 -0
  89. package/dist/host/http.js +30 -0
  90. package/dist/host/http.js.map +1 -0
  91. package/dist/host/index.d.ts +23 -0
  92. package/dist/host/index.js +86 -0
  93. package/dist/host/index.js.map +1 -0
  94. package/dist/host/installed-v10.js +27181 -0
  95. package/dist/host/installed-v11.js +27181 -0
  96. package/dist/host/installed-v12.js +27181 -0
  97. package/dist/host/installed-v13.js +27181 -0
  98. package/dist/host/installed-v2.js +27181 -0
  99. package/dist/host/installed-v3.js +27181 -0
  100. package/dist/host/installed-v4.js +27181 -0
  101. package/dist/host/installed-v5.js +27181 -0
  102. package/dist/host/installed-v6.js +27181 -0
  103. package/dist/host/installed-v7.js +27181 -0
  104. package/dist/host/installed-v8.js +27181 -0
  105. package/dist/host/installed-v9.js +27181 -0
  106. package/dist/host/mcp.d.ts +11 -0
  107. package/dist/host/mcp.js +42 -0
  108. package/dist/host/mcp.js.map +1 -0
  109. package/dist/host/models.d.ts +37 -0
  110. package/dist/host/models.js +141 -0
  111. package/dist/host/models.js.map +1 -0
  112. package/dist/host/operations.d.ts +22 -0
  113. package/dist/host/operations.js +72 -0
  114. package/dist/host/operations.js.map +1 -0
  115. package/dist/host/plugin.js +27181 -0
  116. package/dist/host/policies.d.ts +36 -0
  117. package/dist/host/policies.js +73 -0
  118. package/dist/host/policies.js.map +1 -0
  119. package/dist/host/requests.d.ts +57 -0
  120. package/dist/host/requests.js +353 -0
  121. package/dist/host/requests.js.map +1 -0
  122. package/dist/host/skills.d.ts +2 -0
  123. package/dist/host/skills.js +45 -0
  124. package/dist/host/skills.js.map +1 -0
  125. package/dist/host/tool-client.d.ts +19 -0
  126. package/dist/host/tool-client.js +62 -0
  127. package/dist/host/tool-client.js.map +1 -0
  128. package/dist/host/validation.d.ts +5 -0
  129. package/dist/host/validation.js +42 -0
  130. package/dist/host/validation.js.map +1 -0
  131. package/dist/mcp.d.ts +7 -0
  132. package/dist/mcp.js +2 -0
  133. package/dist/mcp.js.map +1 -0
  134. package/dist/models.d.ts +34 -0
  135. package/dist/models.js +53 -0
  136. package/dist/models.js.map +1 -0
  137. package/dist/monitoring/error-buffer.d.ts +89 -0
  138. package/dist/monitoring/error-buffer.js +355 -0
  139. package/dist/monitoring/error-buffer.js.map +1 -0
  140. package/dist/monitoring/fatal.d.ts +6 -0
  141. package/dist/monitoring/fatal.js +32 -0
  142. package/dist/monitoring/fatal.js.map +1 -0
  143. package/dist/monitoring/index.d.ts +7 -0
  144. package/dist/monitoring/index.js +8 -0
  145. package/dist/monitoring/index.js.map +1 -0
  146. package/dist/monitoring/logger.d.ts +45 -0
  147. package/dist/monitoring/logger.js +174 -0
  148. package/dist/monitoring/logger.js.map +1 -0
  149. package/dist/monitoring/schemas.d.ts +34 -0
  150. package/dist/monitoring/schemas.js +132 -0
  151. package/dist/monitoring/schemas.js.map +1 -0
  152. package/dist/monitoring/standard.d.ts +51 -0
  153. package/dist/monitoring/standard.js +140 -0
  154. package/dist/monitoring/standard.js.map +1 -0
  155. package/dist/monitoring/tailer.d.ts +41 -0
  156. package/dist/monitoring/tailer.js +210 -0
  157. package/dist/monitoring/tailer.js.map +1 -0
  158. package/dist/requests.d.ts +92 -0
  159. package/dist/requests.js +120 -0
  160. package/dist/requests.js.map +1 -0
  161. package/dist/runtime/bootstrap.d.ts +1 -0
  162. package/dist/runtime/bootstrap.js +25 -0
  163. package/dist/runtime/bootstrap.js.map +1 -0
  164. package/dist/runtime/definitions.d.ts +7 -0
  165. package/dist/runtime/definitions.js +138 -0
  166. package/dist/runtime/definitions.js.map +1 -0
  167. package/dist/runtime/index.d.ts +4 -0
  168. package/dist/runtime/index.js +4 -0
  169. package/dist/runtime/index.js.map +1 -0
  170. package/dist/runtime/log-file.d.ts +24 -0
  171. package/dist/runtime/log-file.js +124 -0
  172. package/dist/runtime/log-file.js.map +1 -0
  173. package/dist/runtime/protocol.d.ts +60 -0
  174. package/dist/runtime/protocol.js +155 -0
  175. package/dist/runtime/protocol.js.map +1 -0
  176. package/dist/runtime/registry.d.ts +8 -0
  177. package/dist/runtime/registry.js +38 -0
  178. package/dist/runtime/registry.js.map +1 -0
  179. package/dist/runtime/supervisor.d.ts +74 -0
  180. package/dist/runtime/supervisor.js +881 -0
  181. package/dist/runtime/supervisor.js.map +1 -0
  182. package/dist/sdk/connection.d.ts +33 -0
  183. package/dist/sdk/connection.js +265 -0
  184. package/dist/sdk/connection.js.map +1 -0
  185. package/dist/sdk/index.d.ts +8 -0
  186. package/dist/sdk/index.js +2 -0
  187. package/dist/sdk/index.js.map +1 -0
  188. package/dist/supervision.d.ts +22 -0
  189. package/dist/supervision.js +32 -0
  190. package/dist/supervision.js.map +1 -0
  191. package/dist/types.d.ts +170 -0
  192. package/dist/types.js +2 -0
  193. package/dist/types.js.map +1 -0
  194. package/docs/installation.md +37 -0
  195. package/docs/log-format.md +102 -0
  196. package/docs/publishing.md +45 -0
  197. package/docs/supervision.md +73 -0
  198. package/examples/dashboard.ts +31 -0
  199. package/examples/dsh-web-local.patch.yml +15 -0
  200. package/examples/dynamic-server.ts +13 -0
  201. package/examples/inbox-worker.ts +17 -0
  202. package/examples/model-slots.ts +12 -0
  203. package/examples/standalone-supervisor.ts +14 -0
  204. package/examples/watch-server.ts +12 -0
  205. package/package.json +134 -3
  206. package/schemas/dsh.http.request.v1.schema.json +14 -0
  207. package/schemas/dsh.program.error-batch.v1.schema.json +24 -0
  208. package/schemas/dsh.program.error.v1.schema.json +8 -0
  209. package/schemas/standard-log-record.v1.schema.json +26 -0
  210. package/skills/program-agents/SKILL.md +124 -0
  211. package/skills/program-agents/chat-sidebar.md +23 -0
  212. package/skills/program-agents/endpoints.md +92 -0
  213. package/skills/program-agents/inboxes.md +120 -0
  214. package/skills/program-agents/log-format.md +102 -0
  215. package/skills/program-agents/models.md +59 -0
  216. package/skills/program-agents/references/chat-sidebar/AgentSidebar.tsx +181 -0
  217. package/skills/program-agents/references/chat-sidebar/README.md +181 -0
  218. package/skills/program-agents/references/chat-sidebar/agent-sidebar.css +37 -0
  219. package/skills/program-agents/references/chat-sidebar/backend.ts +184 -0
  220. package/skills/program-agents/references/chat-sidebar/protocol.ts +214 -0
  221. package/skills/program-agents/schemas/dsh.http.request.v1.schema.json +14 -0
  222. package/skills/program-agents/schemas/dsh.program.error-batch.v1.schema.json +24 -0
  223. package/skills/program-agents/schemas/dsh.program.error.v1.schema.json +8 -0
  224. package/skills/program-agents/schemas/standard-log-record.v1.schema.json +26 -0
  225. package/skills/program-agents/ui-patterns.md +39 -0
@@ -0,0 +1,184 @@
1
+ // SERVER ONLY. Never import this module (or connectProgram) into the browser bundle.
2
+ import { connectProgram, type ConversationHandle, type ConversationMessage, type ConversationOpenOptions, type JsonValue, type ProgramConversations } from '@kelloggt/dsh-program-agents/app';
3
+ import type { Context, Hono } from 'hono';
4
+ import { canonicalJson, object, SIDEBAR_LIMITS, utf8Bytes, type NativeFrame } from './protocol.js';
5
+
6
+ export interface AgentSidebarOptions<PageState extends JsonValue> {
7
+ /** Exact public app origin, including port. Do not derive this from incoming Host/Origin. */
8
+ appOrigin: string;
9
+ basePath?: string;
10
+ conversation: ConversationOpenOptions;
11
+ /** Mandatory application allowlist. Treat input as hostile; return a fresh minimal object. */
12
+ sanitizePageState: (input: unknown) => PageState;
13
+ /** Injection for app ownership/testing. Default connectProgram() remains backend-only and lazy. */
14
+ conversations?: ProgramConversations;
15
+ /** Add real app authentication here for non-loopback deployments. CSRF checks are not auth. */
16
+ authorize?: (request: Request) => boolean | Promise<boolean>;
17
+ /** Log codes/operation IDs, not raw prompt/context, credentials, headers, or arbitrary stacks. */
18
+ onError?: (operation: string, error: unknown) => void;
19
+ }
20
+ class HttpFailure extends Error { constructor(readonly status: 400 | 401 | 403 | 413 | 415 | 429, message: string) { super(message); } }
21
+ function integer(value: string | undefined, fallback?: number): number {
22
+ if (value === undefined && fallback !== undefined) return fallback;
23
+ if (!value || !/^-?\d+$/.test(value) || !Number.isSafeInteger(Number(value))) throw new HttpFailure(400, 'Invalid history cursor');
24
+ return Number(value);
25
+ }
26
+ async function readJson(request: Request): Promise<unknown> {
27
+ if (request.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !== 'application/json') throw new HttpFailure(415, 'Expected application/json');
28
+ const length = request.headers.get('content-length');
29
+ if (length !== null && (!/^\d+$/.test(length) || Number(length) > SIDEBAR_LIMITS.requestBytes)) throw new HttpFailure(413, 'Request exceeds 32 KiB');
30
+ if (!request.body) throw new HttpFailure(400, 'Expected JSON body');
31
+ const reader = request.body.getReader();
32
+ const chunks: Uint8Array[] = []; let size = 0;
33
+ try {
34
+ for (;;) {
35
+ const item = await reader.read(); if (item.done) break;
36
+ size += item.value.byteLength;
37
+ if (size > SIDEBAR_LIMITS.requestBytes) { await reader.cancel(); throw new HttpFailure(413, 'Request exceeds 32 KiB'); }
38
+ chunks.push(item.value);
39
+ }
40
+ } finally { reader.releaseLock(); }
41
+ const bytes = new Uint8Array(size); let offset = 0;
42
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
43
+ try { return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); }
44
+ catch { throw new HttpFailure(400, 'Invalid JSON'); }
45
+ }
46
+ function messageInput<PageState extends JsonValue>(input: unknown, sanitize: (value: unknown) => PageState): { message: ConversationMessage; requestId: string } {
47
+ if (!object(input) || typeof input.text !== 'string' || !input.text.trim() || typeof input.requestId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(input.requestId)) throw new HttpFailure(400, 'Expected text and an opaque requestId');
48
+ if (Object.keys(input).some(key => !['text', 'requestId', 'context'].includes(key))) throw new HttpFailure(400, 'Unknown message field');
49
+ const message: ConversationMessage = { text: input.text };
50
+ if (input.context !== undefined) {
51
+ if (!object(input.context) || !Object.hasOwn(input.context, 'state') || Object.keys(input.context).some(key => !['state', 'revision'].includes(key))) throw new HttpFailure(400, 'Invalid page context');
52
+ const revision = input.context.revision;
53
+ if (revision !== undefined && !((typeof revision === 'string' && revision.length <= 256) || (typeof revision === 'number' && Number.isSafeInteger(revision)))) throw new HttpFailure(400, 'Invalid page revision');
54
+ try {
55
+ // Bound both the submitted state and the allowlisted output, measured in UTF-8 bytes.
56
+ if (utf8Bytes(canonicalJson(input.context.state)) > SIDEBAR_LIMITS.stateBytes) throw new HttpFailure(413, 'Page state exceeds 16 KiB');
57
+ const state = sanitize(input.context.state);
58
+ const serialized = canonicalJson(state);
59
+ if (utf8Bytes(serialized) > SIDEBAR_LIMITS.stateBytes) throw new HttpFailure(413, 'Page state exceeds 16 KiB');
60
+ message.context = { ...(revision === undefined ? {} : { revision }), state: JSON.parse(serialized) as PageState };
61
+ // Native context admission also counts the small revision/state envelope.
62
+ if (utf8Bytes(canonicalJson(message.context)) > SIDEBAR_LIMITS.stateBytes) throw new HttpFailure(413, 'Page context envelope exceeds 16 KiB');
63
+ } catch (error) {
64
+ if (error instanceof HttpFailure) throw error;
65
+ throw new HttpFailure(400, 'Page state failed the application allowlist');
66
+ }
67
+ }
68
+ return { message, requestId: input.requestId };
69
+ }
70
+
71
+ /** Mount one app-owned conversation, not an arbitrary session/bridge proxy. No startup child. */
72
+ export function mountAgentSidebar<PageState extends JsonValue>(app: Hono, options: AgentSidebarOptions<PageState>) {
73
+ const origin = new URL(options.appOrigin);
74
+ if (!['http:', 'https:'].includes(origin.protocol) || origin.origin !== options.appOrigin || origin.username || origin.password) throw new Error('appOrigin must be an exact HTTP(S) origin');
75
+ const basePath = options.basePath ?? '/api/assistant';
76
+ if (!/^\/[A-Za-z0-9/_-]+$/.test(basePath) || basePath.endsWith('/')) throw new Error('Invalid sidebar basePath');
77
+ // Freeze creation identity/instructions. Changing them does not rewrite an existing child.
78
+ const conversationOptions = { ...options.conversation };
79
+ let singleton: Promise<ConversationHandle> | undefined;
80
+ let activeStreams = 0;
81
+ let pendingSends = 0;
82
+ const conversation = (): Promise<ConversationHandle> => {
83
+ if (!singleton) {
84
+ singleton = Promise.resolve().then(() => (options.conversations ?? connectProgram().conversations).open(conversationOptions));
85
+ // A rejected open may be retried on a later explicit chat request, never in a startup loop.
86
+ void singleton.catch(() => { singleton = undefined; });
87
+ }
88
+ return singleton;
89
+ };
90
+ app.use(`${basePath}/*`, async (c, next) => {
91
+ c.header('Cache-Control', 'no-store'); c.header('X-Content-Type-Options', 'nosniff');
92
+ const request = c.req.raw;
93
+ const host = request.headers.get('host');
94
+ if (new URL(request.url).origin !== origin.origin || (host !== null && host.toLowerCase() !== origin.host)) return c.json({ error: 'Wrong app host' }, 403);
95
+ const suppliedOrigin = request.headers.get('origin');
96
+ const explicitSameOrigin = suppliedOrigin === options.appOrigin;
97
+ // Browsers omit Origin on same-origin GET and forbid JavaScript from setting it.
98
+ // Only this GET-only Fetch-Metadata + non-simple-header exception is permitted.
99
+ const sameOriginGet = request.method === 'GET' && suppliedOrigin === null && request.headers.get('sec-fetch-site') === 'same-origin';
100
+ if ((!explicitSameOrigin && !sameOriginGet) || request.headers.get('x-agent-sidebar') !== '1') return c.json({ error: 'Same-origin sidebar request required' }, 403);
101
+ if (options.authorize && !await options.authorize(request)) return c.json({ error: 'Authentication required' }, 401);
102
+ await next();
103
+ });
104
+ const guarded = (operation: string, handler: (c: Context) => Promise<Response>) => async (c: Context): Promise<Response> => {
105
+ try { return await handler(c); }
106
+ catch (error) {
107
+ if (error instanceof HttpFailure) return c.json({ error: error.message }, error.status);
108
+ options.onError?.(operation, error);
109
+ // A bridge error can contain private details. The browser sees only this stable summary.
110
+ return c.json({ error: `${operation} failed; inspect history before retrying a send` }, 502);
111
+ }
112
+ };
113
+ app.post(`${basePath}/messages`, guarded('Send', async c => {
114
+ const input = messageInput(await readJson(c.req.raw), options.sanitizePageState);
115
+ if (pendingSends >= 8) throw new HttpFailure(429, 'Too many pending sends');
116
+ pendingSends++;
117
+ try {
118
+ const handle = await conversation();
119
+ // No retries: a timeout/disconnect can happen after native admission. Always queue a new turn.
120
+ const receipt = await handle.send(input.message, { requestId: input.requestId, mode: 'queue', signal: c.req.raw.signal });
121
+ return c.json(receipt, 202);
122
+ } finally { pendingSends--; }
123
+ }));
124
+ app.get(`${basePath}/history`, guarded('History', async c => {
125
+ const throughSeq = integer(c.req.query('throughSeq'));
126
+ const beforeSeq = c.req.query('beforeSeq') === undefined ? undefined : integer(c.req.query('beforeSeq'));
127
+ const maxMessages = integer(c.req.query('maxMessages'), 20);
128
+ if (throughSeq < -1 || (beforeSeq !== undefined && (beforeSeq < 0 || beforeSeq > throughSeq + 1)) || maxMessages < 1 || maxMessages > 100) throw new HttpFailure(400, 'Invalid history range');
129
+ const handle = await conversation();
130
+ return c.json(await handle.history({ throughSeq, ...(beforeSeq === undefined ? {} : { beforeSeq }), maxMessages }, { signal: c.req.raw.signal }));
131
+ }));
132
+ app.post(`${basePath}/interrupt`, guarded('Interrupt', async c => {
133
+ const body = await readJson(c.req.raw);
134
+ if (!object(body) || Object.keys(body).length) throw new HttpFailure(400, 'Expected empty JSON object');
135
+ // Interrupting an unopened sidebar is a no-op, not a reason to create a child.
136
+ if (!singleton) return c.json({ accepted: true });
137
+ return c.json(await (await singleton).interrupt({ signal: c.req.raw.signal }));
138
+ }));
139
+ app.get(`${basePath}/events`, guarded('Subscribe', async c => {
140
+ if (activeStreams >= 2) throw new HttpFailure(429, 'Too many sidebar subscriptions');
141
+ activeStreams++;
142
+ const abort = new AbortController();
143
+ let iterator: AsyncIterator<NativeFrame> | undefined;
144
+ let closing: Promise<IteratorResult<NativeFrame>> | undefined;
145
+ const closeIterator = () => closing ??= iterator?.return?.();
146
+ let ended = false;
147
+ const cleanup = () => {
148
+ if (ended) return;
149
+ ended = true; activeStreams--; abort.abort();
150
+ c.req.raw.signal.removeEventListener('abort', requestAbort);
151
+ // Release even when the browser disconnects between pulls (no pending next()).
152
+ void closeIterator()?.catch(() => {});
153
+ };
154
+ const requestAbort = () => cleanup();
155
+ c.req.raw.signal.addEventListener('abort', requestAbort, { once: true });
156
+ if (c.req.raw.signal.aborted) cleanup();
157
+ try {
158
+ const handle = await conversation();
159
+ iterator = handle.events({ signal: abort.signal, maxMessages: 20 })[Symbol.asyncIterator]();
160
+ if (abort.signal.aborted) { await closeIterator(); throw new Error('Subscription cancelled'); }
161
+ const encoder = new TextEncoder();
162
+ // Pull, don't eagerly drain native events into an unbounded application queue.
163
+ // The bridge also bounds its queue and eventually disconnects a slow browser.
164
+ const body = new ReadableStream<Uint8Array>({
165
+ async pull(controller) {
166
+ try {
167
+ const item = await iterator!.next();
168
+ if (item.done) { cleanup(); controller.close(); return; }
169
+ const json = JSON.stringify(item.value);
170
+ if (utf8Bytes(json) > SIDEBAR_LIMITS.frameBytes) throw new Error('Native frame exceeds sidebar limit');
171
+ controller.enqueue(encoder.encode(`event: native\ndata: ${json}\n\n`));
172
+ } catch (error) {
173
+ const wasAborted = abort.signal.aborted; cleanup();
174
+ if (!wasAborted) options.onError?.('Subscribe', error);
175
+ controller.error(new Error('Assistant subscription disconnected'));
176
+ }
177
+ },
178
+ async cancel() { cleanup(); await closeIterator(); },
179
+ }, { highWaterMark: 0 });
180
+ return new Response(body, { headers: { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-store', 'X-Accel-Buffering': 'no', 'X-Content-Type-Options': 'nosniff' } });
181
+ } catch (error) { cleanup(); throw error; }
182
+ }));
183
+ return { basePath };
184
+ }
@@ -0,0 +1,214 @@
1
+ // Browser-safe: all SDK imports are types; no bridge client or Node builtins here.
2
+ import type { ConversationFrame, JsonValue, SessionFollowFrame, SessionPage } from '@kelloggt/dsh-program-agents/app';
3
+
4
+ export type NativeFrame = SessionFollowFrame;
5
+ export type NativeEvent = Extract<NativeFrame, { type: 'event' }>['event'];
6
+ type Snapshot = Extract<NativeFrame, { type: 'snapshot' }>;
7
+ export type PageContext = { revision?: string | number; state: JsonValue };
8
+ export const SIDEBAR_LIMITS = Object.freeze({ requestBytes: 32 * 1024, stateBytes: 16 * 1024, frameBytes: 512 * 1024, records: 1000, liveCharacters: 256 * 1024 });
9
+ export type DisplayBlock = { type: 'text' | 'reasoning'; text: string } | { type: 'tool-call'; id: string; name: string; arguments: string };
10
+ export interface LiveAttempt {
11
+ attemptId: string; turn: number; step: number; startedAfterSeq: number; nextIndex: number;
12
+ blocks: Readonly<Record<number, DisplayBlock>>;
13
+ }
14
+ export interface SidebarState {
15
+ sessionId?: string;
16
+ /** Fixed pagination cut from the most recent snapshot, not the moving live cursor. */
17
+ throughSeq: number;
18
+ cursor: number;
19
+ records: readonly NativeEvent[];
20
+ hasMore: boolean;
21
+ streamRevision: number;
22
+ active?: LiveAttempt;
23
+ status: 'idle' | 'running';
24
+ /** Native ask receipts are correlation, not an exactly-once guarantee. */
25
+ receipts: Readonly<Record<string, string>>;
26
+ }
27
+ export const initialSidebarState = (): SidebarState => ({ throughSeq: -1, cursor: -1, records: [], hasMore: false, streamRevision: 0, status: 'idle', receipts: {} });
28
+ export function object(value: unknown): value is Record<string, unknown> { return value !== null && typeof value === 'object' && !Array.isArray(value); }
29
+ export function utf8Bytes(value: string): number { return new TextEncoder().encode(value).byteLength; }
30
+
31
+ /** Deterministic JSON data only. Useful for app-side revision fingerprints, NOT instructions. */
32
+ export function canonicalJson(value: unknown, depth = 0): string {
33
+ if (depth > 32) throw new Error('JSON is too deeply nested');
34
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value);
35
+ if (typeof value === 'number' && Number.isFinite(value)) return JSON.stringify(value);
36
+ if (Array.isArray(value)) return '[' + value.map(item => canonicalJson(item, depth + 1)).join(',') + ']';
37
+ if (object(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) {
38
+ return '{' + Object.keys(value).sort().map(key => JSON.stringify(key) + ':' + canonicalJson(value[key], depth + 1)).join(',') + '}';
39
+ }
40
+ throw new Error('Expected plain JSON data');
41
+ }
42
+
43
+ function block(value: unknown): DisplayBlock | undefined {
44
+ if (!object(value)) return;
45
+ if ((value.type === 'text' || value.type === 'reasoning') && typeof value.text === 'string') return { type: value.type, text: value.text };
46
+ if (value.type === 'tool-call' && typeof value.id === 'string' && typeof value.name === 'string' && typeof value.arguments === 'string') return { type: value.type, id: value.id, name: value.name, arguments: value.arguments };
47
+ }
48
+ /** Native text-delta uses text, not delta; block-end replaces accumulated text. */
49
+ function foldChunk(blocks: Readonly<Record<number, DisplayBlock>>, value: unknown): Readonly<Record<number, DisplayBlock>> {
50
+ if (!object(value) || !Number.isSafeInteger(value.index) || (value.index as number) < 0) return blocks;
51
+ const index = value.index as number;
52
+ if (index > 1024) throw new Error('Too many stream blocks; reconnect');
53
+ const previous = blocks[index];
54
+ let next: DisplayBlock | undefined;
55
+ if ((value.type === 'text-delta' || value.type === 'reasoning-delta') && typeof value.text === 'string') {
56
+ const type = value.type === 'text-delta' ? 'text' : 'reasoning';
57
+ next = { type, text: (previous?.type === type ? previous.text : '') + value.text };
58
+ } else if (value.type === 'tool-call-delta' && typeof value.id === 'string' && typeof value.argumentsDelta === 'string') {
59
+ next = { type: 'tool-call', id: value.id, name: typeof value.name === 'string' ? value.name : previous?.type === 'tool-call' ? previous.name : 'Tool', arguments: (previous?.type === 'tool-call' ? previous.arguments : '') + value.argumentsDelta };
60
+ } else if (value.type === 'block-end') next = block(value.block);
61
+ if (!next) return blocks;
62
+ const result = { ...blocks, [index]: next };
63
+ if (Object.values(result).reduce((size, item) => size + (item.type === 'tool-call' ? item.arguments.length + item.name.length : item.text.length), 0) > SIDEBAR_LIMITS.liveCharacters) throw new Error('Live output limit reached; reconnect');
64
+ return result;
65
+ }
66
+
67
+ /** Native snapshot.stream is compact, unlike live chunk frames. Do not render it as deltas. */
68
+ function expandCompact(records: readonly unknown[]): unknown[] {
69
+ const result: unknown[] = [];
70
+ for (const record of records) {
71
+ if (!object(record)) throw new Error('Invalid compact stream record');
72
+ if (record.type === 'chunk') result.push(record.chunk);
73
+ else if ((record.type === 'text-chunks' || record.type === 'reasoning-chunks') && Array.isArray(record.texts)) {
74
+ for (const text of record.texts) result.push({ type: record.type === 'text-chunks' ? 'text-delta' : 'reasoning-delta', index: record.index, text });
75
+ } else if (record.type === 'tool-call-chunks' && Array.isArray(record.args)) {
76
+ for (const argumentsDelta of record.args) result.push({ type: 'tool-call-delta', index: record.index, id: record.id, name: record.name, argumentsDelta });
77
+ } else throw new Error('Unknown compact stream record; update the reference');
78
+ if (result.length > 100_000) throw new Error('Compact stream exceeds reference limit');
79
+ }
80
+ return result;
81
+ }
82
+ function baseline(snapshot: Snapshot): LiveAttempt | undefined {
83
+ const attempt = snapshot.assistantStream?.activeAttempt;
84
+ if (!attempt) return;
85
+ const chunks = expandCompact(attempt.stream);
86
+ if (chunks.length !== attempt.nextIndex) throw new Error('Invalid native attempt baseline');
87
+ return { attemptId: attempt.attemptId, turn: attempt.turn, step: attempt.step, startedAfterSeq: attempt.startedAfterSeq, nextIndex: attempt.nextIndex, blocks: chunks.reduce<Readonly<Record<number, DisplayBlock>>>((blocks, chunk) => foldChunk(blocks, chunk), {}) };
88
+ }
89
+ function statusAfter(status: SidebarState['status'], event: NativeEvent): SidebarState['status'] {
90
+ if (event.type === 'turn/start') return 'running';
91
+ if (event.type === 'turn/end') return 'idle';
92
+ return status;
93
+ }
94
+ function mergeRecords(records: readonly NativeEvent[], additions: readonly NativeEvent[]): NativeEvent[] {
95
+ const bySeq = new Map(records.map(event => [event.seq, event]));
96
+ for (const event of additions) bySeq.set(event.seq, event);
97
+ const result = [...bySeq.values()].sort((a, b) => a.seq - b.seq);
98
+ if (result.length > SIDEBAR_LIMITS.records) throw new Error('Transcript window full; reconnect for a fresh bounded window');
99
+ return result;
100
+ }
101
+
102
+ /** Pure fold of the installed native SessionFollowFrame vocabulary. Snapshot is replacement. */
103
+ export function reduceFrame(state: SidebarState, frame: ConversationFrame): SidebarState {
104
+ if (frame.type === 'receipt') return { ...state, receipts: Object.fromEntries(Object.entries({ ...state.receipts, [frame.requestId]: frame.messageId }).slice(-128)) };
105
+ if (frame.type === 'snapshot') {
106
+ const records = mergeRecords([], frame.records.map(record => record.event));
107
+ const active = baseline(frame);
108
+ return { ...initialSidebarState(), sessionId: frame.header.id, throughSeq: frame.cursor, cursor: frame.cursor, records, hasMore: frame.hasMore, streamRevision: frame.assistantStream?.revision ?? 0, active, status: active ? 'running' : records.reduce(statusAfter, 'idle'), receipts: state.receipts };
109
+ }
110
+ if (!state.sessionId) throw new Error('Expected a native snapshot first');
111
+ if (frame.type === 'event') {
112
+ if (frame.event.seq <= state.cursor) return state; // repeated durable entry, not repeated token deltas
113
+ if (frame.event.seq !== state.cursor + 1) throw new Error('Durable stream gap; reconnect');
114
+ return { ...state, cursor: frame.event.seq, records: mergeRecords(state.records, [frame.event]), status: statusAfter(state.status, frame.event) };
115
+ }
116
+ const live = frame.frame;
117
+ // Agent replacement restarts its process-local revision counter. Never keep a stale prefix.
118
+ if (live.type === 'start' && live.revision === 1 && state.streamRevision !== 0) throw new Error('Assistant lifecycle reset; reconnect');
119
+ if (live.revision <= state.streamRevision) return state;
120
+ if (live.revision !== state.streamRevision + 1) throw new Error('Assistant stream gap; reconnect');
121
+ const updated = { ...state, streamRevision: live.revision };
122
+ if (live.type === 'start') {
123
+ if (state.active) throw new Error('Overlapping attempt; reconnect');
124
+ return { ...updated, active: { attemptId: live.attemptId, turn: live.turn, step: live.step, startedAfterSeq: live.startedAfterSeq, nextIndex: 0, blocks: {} } };
125
+ }
126
+ const active = state.active;
127
+ // The host may attach after a start. Unknown transient suffixes cannot be reconstructed.
128
+ if (!active || active.attemptId !== live.attemptId) return updated;
129
+ if (live.index !== active.nextIndex) throw new Error('Assistant chunk gap; reconnect');
130
+ if (live.type === 'chunk') return { ...updated, active: { ...active, nextIndex: active.nextIndex + 1, blocks: foldChunk(active.blocks, live.chunk) } };
131
+ if (live.outcome.kind === 'committed') {
132
+ const settlementSeq = live.outcome.seq;
133
+ const settlement = state.records.find(event => event.seq === settlementSeq);
134
+ if (!settlement || settlement.type !== live.outcome.eventType || !object(settlement.data) || settlement.data.turn !== active.turn || settlement.data.step !== active.step) throw new Error('Missing native attempt settlement; reconnect');
135
+ }
136
+ // Durable settlement seq is authoritative; it replaces this exact transient attempt.
137
+ return { ...updated, active: undefined };
138
+ }
139
+ export function prependHistory(state: SidebarState, page: SessionPage, throughSeq: number): SidebarState {
140
+ if (throughSeq !== state.throughSeq) return state; // a reconnect superseded the in-flight page
141
+ return { ...state, records: mergeRecords(state.records, page.records.map(record => record.event).filter(event => event.seq <= throughSeq)), hasMore: page.hasMore };
142
+ }
143
+ export interface TranscriptRow { key: string; role: 'user' | 'assistant' | 'tool' | 'status'; blocks: readonly DisplayBlock[]; messageId?: string; requestId?: string; transient?: boolean }
144
+ function messageBlocks(value: unknown): DisplayBlock[] {
145
+ if (!object(value) || !Array.isArray(value.content)) return [];
146
+ return value.content.flatMap(value => { const parsed = block(value); return parsed ? [parsed] : []; });
147
+ }
148
+ export function transcriptRows(state: SidebarState): TranscriptRow[] {
149
+ const rows: TranscriptRow[] = [];
150
+ for (const event of state.records) {
151
+ const data = object(event.data) ? event.data : {};
152
+ // Human transcript keeps append-origin events; compaction/replacement copies are model-only.
153
+ if ((event.type === 'user/message' || event.type === 'assistant/message' || event.type === 'tool/result') && event.surfaceOp !== 'append') continue;
154
+ if (event.type === 'assistant/message' && state.active && event.seq > state.active.startedAfterSeq && data.turn === state.active.turn && data.step === state.active.step) continue; // staged until end.outcome.seq retires the live attempt
155
+ if (event.type === 'user/message' || event.type === 'assistant/message' || event.type === 'tool/result') {
156
+ const message = event.type === 'user/message' ? data : data.message;
157
+ const source = object(message) && object(message.source) ? message.source : {};
158
+ rows.push({ key: `event:${event.seq}`, role: event.type === 'user/message' ? 'user' : event.type === 'assistant/message' ? 'assistant' : 'tool', blocks: messageBlocks(message), messageId: object(message) && typeof message.id === 'string' ? message.id : undefined, requestId: typeof source.rpcId === 'string' ? source.rpcId : undefined });
159
+ } else if (event.type === 'tool/call' && typeof data.callId === 'string' && typeof data.name === 'string' && typeof data.arguments === 'string') {
160
+ // Many assistants also carry tool-call blocks: show the explicit call only when absent there.
161
+ if (!rows.some(row => row.blocks.some(block => block.type === 'tool-call' && block.id === data.callId))) rows.push({ key: `event:${event.seq}`, role: 'tool', blocks: [{ type: 'tool-call', id: data.callId, name: data.name, arguments: data.arguments }] });
162
+ } else if (event.type === 'turn/end') {
163
+ const reason = object(data.reason) && typeof data.reason.kind === 'string' ? data.reason.kind : 'finished';
164
+ rows.push({ key: `event:${event.seq}`, role: 'status', blocks: [{ type: 'text', text: `Turn ${reason}` }] });
165
+ } else if (event.type === 'assistant/attempt') rows.push({ key: `event:${event.seq}`, role: 'status', blocks: [{ type: 'text', text: 'Model attempt settled without a message' }] });
166
+ }
167
+ if (state.active) rows.push({ key: `attempt:${state.active.attemptId}`, role: 'assistant', transient: true, blocks: Object.values(state.active.blocks) });
168
+ return rows;
169
+ }
170
+
171
+ /** Bounded fetch-SSE reader. Reconnection opens a NEW snapshot; there is no Last-Event-ID replay. */
172
+ export async function consumeNativeSSE(response: Response, accept: (frame: NativeFrame) => void, signal: AbortSignal): Promise<void> {
173
+ if (!response.ok || !response.headers.get('content-type')?.includes('text/event-stream') || !response.body) throw new Error(`Assistant stream unavailable (${response.status})`);
174
+ const reader = response.body.getReader();
175
+ const decoder = new TextDecoder('utf-8', { fatal: true });
176
+ let buffer = '';
177
+ const abort = () => { void reader.cancel().catch(() => {}); };
178
+ signal.addEventListener('abort', abort, { once: true });
179
+ if (signal.aborted) abort();
180
+ const deliver = (packet: string) => {
181
+ let event = 'message'; const data: string[] = [];
182
+ for (const line of packet.split('\n')) {
183
+ if (line.startsWith('event:')) event = line.slice(6).trimStart();
184
+ if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /, ''));
185
+ }
186
+ if (!data.length) return;
187
+ if (event === 'sidebar-error') throw new Error('Assistant stream disconnected; reconnect to inspect history');
188
+ if (event !== 'native') return;
189
+ const frame: unknown = JSON.parse(data.join('\n'));
190
+ if (!object(frame) || !['snapshot', 'event', 'assistant-stream'].includes(String(frame.type))) throw new Error('Invalid native frame');
191
+ // Native bridge is the schema-validation boundary; never interpret arbitrary HTML here.
192
+ accept(frame as unknown as NativeFrame);
193
+ };
194
+ try {
195
+ while (!signal.aborted) {
196
+ const item = await reader.read();
197
+ if (item.done) { buffer += decoder.decode(); break; }
198
+ buffer += decoder.decode(item.value, { stream: true });
199
+ // CRLF may be split across reads. Normalize complete packets, not each chunk.
200
+ let match: RegExpExecArray | null;
201
+ while ((match = /\r?\n\r?\n/.exec(buffer))) {
202
+ const packet = buffer.slice(0, match.index).replace(/\r\n/g, '\n');
203
+ if (utf8Bytes(packet) > SIDEBAR_LIMITS.frameBytes + 128) throw new Error('Assistant frame exceeds limit');
204
+ buffer = buffer.slice(match.index + match[0].length); deliver(packet);
205
+ }
206
+ if (utf8Bytes(buffer) > SIDEBAR_LIMITS.frameBytes + 128) throw new Error('Assistant frame exceeds limit');
207
+ }
208
+ } finally {
209
+ signal.removeEventListener('abort', abort);
210
+ await reader.cancel().catch(() => {}); reader.releaseLock();
211
+ }
212
+ if (signal.aborted) throw new DOMException('Assistant subscription cancelled', 'AbortError');
213
+ // An incomplete last frame is never committed. Caller reconnects on EOF.
214
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://dsh.dev/schemas/dsh.http.request.v1.schema.json",
4
+ "title": "dsh.http.request.v1 required data payload",
5
+ "type": "object",
6
+ "required": ["method", "path", "status"],
7
+ "properties": {
8
+ "method": {"type": "string", "minLength": 1, "maxLength": 32},
9
+ "path": {"type": "string"},
10
+ "status": {"type": "integer", "minimum": 100, "maximum": 599},
11
+ "duration_ms": {"type": "number", "minimum": 0}
12
+ },
13
+ "additionalProperties": true
14
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://dsh.dev/schemas/dsh.program.error-batch.v1.schema.json",
4
+ "title": "dsh.program.error-batch.v1 required data payload",
5
+ "type": "object", "required": ["key", "groups", "count", "range", "dropped"], "additionalProperties": true,
6
+ "properties": {
7
+ "key": {"type": "string"}, "count": {"type": "integer", "minimum": 1}, "dropped": {"type": "integer", "minimum": 0},
8
+ "range": {"type": "object", "required": ["first", "last"], "properties": {"first": {"type": "string"}, "last": {"type": "string"}}, "additionalProperties": false},
9
+ "groups": {
10
+ "type": "array", "minItems": 1, "maxItems": 20,
11
+ "items": {
12
+ "type": "object", "required": ["fingerprint", "count", "sample", "range"], "additionalProperties": true,
13
+ "properties": {
14
+ "fingerprint": {"type": "string"}, "count": {"type": "integer", "minimum": 1}, "sample": {"type": "object", "description": "Redacted representative standard record; large payloads/stacks/extensions may be omitted."},
15
+ "range": {"type": "object", "required": ["first", "last"], "properties": {"first": {"type": "string"}, "last": {"type": "string"}}, "additionalProperties": false},
16
+ "source": {
17
+ "type": "object", "additionalProperties": false,
18
+ "properties": {"path": {"type": "string"}, "offset": {"type": "integer", "minimum": 0}, "run": {"oneOf": [{"type": "string"}, {"type": "object", "required": ["id", "generation"], "properties": {"id": {"type": "string"}, "generation": {"type": "integer", "minimum": 0}}, "additionalProperties": false}]}}
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://dsh.dev/schemas/dsh.program.error.v1.schema.json",
4
+ "title": "dsh.program.error.v1 optional data payload",
5
+ "description": "Payload schema only, not the full record. data may be omitted. Additional application/error metadata is allowed; errors under any other log_type are still monitored by ERROR level.",
6
+ "type": "object",
7
+ "additionalProperties": true
8
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://dsh.dev/schemas/standard-log-record.v1.schema.json",
4
+ "title": "DSH extensible standard JSONL record v1",
5
+ "description": "One UTF-8 JSON object per line; at most 65536 bytes, JSON depth at most 32. Runtime rejects non-finite/lossy JSON. Unknown versioned log types are accepted by this base contract.",
6
+ "type": "object",
7
+ "required": ["timestamp", "level", "log_type", "message"],
8
+ "properties": {
9
+ "timestamp": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})$"},
10
+ "level": {"enum": ["DEBUG", "INFO", "WARNING", "ERROR"]},
11
+ "log_type": {"type": "string", "maxLength": 256, "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z][a-z0-9_-]*)+\\.v[1-9][0-9]*$"},
12
+ "message": {"type": "string"},
13
+ "operation_id": {"type": "string"},
14
+ "event_id": {"type": "string"},
15
+ "fatal": {"type": "boolean", "description": "Advisory fatal/unhandled metadata; ERROR level determines monitoring independently."},
16
+ "error": {
17
+ "type": "object", "required": ["name", "message"], "additionalProperties": true,
18
+ "properties": {
19
+ "name": {"type": "string"}, "message": {"type": "string"}, "stack": {"type": "string"},
20
+ "code": {"type": ["string", "number"]}, "fingerprint": {"type": "string"}
21
+ }
22
+ },
23
+ "data": {}
24
+ },
25
+ "additionalProperties": true
26
+ }
@@ -0,0 +1,39 @@
1
+ # UI Patterns
2
+
3
+ This file documents and gives ideas for how to use UI to interact with the user. You are by no means limited to these ideas. These are just suggestions.
4
+
5
+ Overarching themes:
6
+ - Reduce text
7
+ - Use more visuals & interactive components
8
+ - Simplicity is good
9
+ - Challenge yourself in creating richer & more explanatory UI
10
+
11
+ ## Dashboards
12
+ Create a static HTML with image plots & widgets.
13
+
14
+ Suggested tech stack
15
+ * DuckDB-Wasm for in-browser processing
16
+ * plotly.js
17
+ * Use DuckDB on the backend, or simple datasets, and stream parquet to the browser where most data processing happens
18
+
19
+
20
+ ## Documents
21
+ Establish a common theme for the user, maybe their corporate marketing theme, and stick to it. Make heavy use of visuals to explain. Think hard about what could explain a concept more clearly.
22
+
23
+ Include annotate.js and do something with the comments. This is a fantastic way to communicate clearly with the user. Wire up annotate.js with a custom event listener when localStorage is updated (or create a button! better!). Setup a backend endpoint to handle the comments and funnel them to an agent as a message, or send them to collaborators. However the user wants it.
24
+
25
+ ## Interactive diagrams
26
+ Use SVG, HTML, CSS, and JS to create rich and detailed diagrams. Think VERY hard on how a diagram can be made even simpler, or explain a concept more clearly. Remember that simpler explanations almost always are more clear.
27
+
28
+ Consider using a Three.js scene, especially when the data or concept needs more dimensions.
29
+
30
+ ## Live data / Polling
31
+ If there's ever a need for live or up-to-date data, use React and SSE heavily. As always with live data, be wary of how much memory you're consuming. Shedding data to the backend is a reasonable choice. Prefer to do more on the frontend unless it can't happen on the backend (e.g. polling the filesystem, web services).
32
+
33
+ ## Asking questions
34
+ Sometimes questions involve mediums that you, the agent can't see, like video or audio. And so writing an app for the human to help you out is a perfectly sensible thing to do.
35
+
36
+ In addition to documents, consider how writing a program agent can leverage the user to a much greater extent.
37
+
38
+ ## Wire in work tools
39
+ Poll github issues, reach out to people over Slack/Teams, integrate with apps. A program agent is a great way to monitor things that are happening in the world around the user. If you can tighten their touch points for them, you'll increase your own ability to see what's going on and anticipate their needs.