@hachej/boring-agent 0.4.37 → 0.4.39

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.
@@ -0,0 +1,33 @@
1
+ import type { ResourceLocator } from '@hachej/boring-files';
2
+ import type { TaskArtifactDescriptor } from './task-artifacts.js';
3
+ export type PresentedArtifact = ArtifactDescriptor | TaskArtifactDescriptor;
4
+ /** Parse file or host-task presentation without treating a record as a workspace file. */
5
+ export declare function parsePresentedArtifact(value: unknown): PresentedArtifact | undefined;
6
+ export declare const ARTIFACT_TYPES: readonly ["markdown", "html", "svg", "code", "canvas"];
7
+ export type ArtifactType = typeof ARTIFACT_TYPES[number];
8
+ /**
9
+ * What a tool result carries under its `artifact` key. The descriptor of `present` points at a workspace file (`target`), names the
10
+ * revision shown and carries no `id` or `ordinal` (both stay optional for hosts that number their own versions).
11
+ */
12
+ export interface ArtifactDescriptor {
13
+ readonly schema: 'boring.artifact';
14
+ readonly version: 1;
15
+ readonly id?: string;
16
+ readonly title: string;
17
+ readonly type: ArtifactType;
18
+ readonly mediaType: string;
19
+ readonly language?: string;
20
+ readonly target: ResourceLocator;
21
+ readonly revision: string;
22
+ readonly ordinal?: number;
23
+ }
24
+ export type ArtifactDescriptorInput = Omit<ArtifactDescriptor, 'schema' | 'version'>;
25
+ /** Build a descriptor, refusing anything `parseArtifact` would refuse. */
26
+ export declare function createArtifactDescriptor(input: ArtifactDescriptorInput): ArtifactDescriptor;
27
+ /**
28
+ * Strict shape check of an untrusted value (a model-visible tool result, a stored reference). Returns a fresh copy or
29
+ * undefined. This function has no Pi runtime dependency; `registry/pi-artifacts/artifact.ts` carries the same validator for
30
+ * the browser, because a copied registry item cannot import this package.
31
+ */
32
+ export declare function parseArtifact(value: unknown): ArtifactDescriptor | undefined;
33
+ //# sourceMappingURL=artifact-descriptor.d.ts.map
@@ -0,0 +1,59 @@
1
+ import { parseTaskArtifact } from './task-artifacts.js';
2
+ /** Parse file or host-task presentation without treating a record as a workspace file. */
3
+ export function parsePresentedArtifact(value) { return parseArtifact(value) ?? parseTaskArtifact(value); }
4
+ export const ARTIFACT_TYPES = ['markdown', 'html', 'svg', 'code', 'canvas'];
5
+ const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
6
+ const LANGUAGE = /^[A-Za-z0-9+#._-]{1,32}$/;
7
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
8
+ const hasOnly = (value, keys) => Object.keys(value).every(key => keys.includes(key));
9
+ /** Build a descriptor, refusing anything `parseArtifact` would refuse. */
10
+ export function createArtifactDescriptor(input) {
11
+ const descriptor = { schema: 'boring.artifact', version: 1, ...input };
12
+ const parsed = parseArtifact(descriptor);
13
+ if (!parsed)
14
+ throw new TypeError('Invalid artifact descriptor');
15
+ return parsed;
16
+ }
17
+ /**
18
+ * Strict shape check of an untrusted value (a model-visible tool result, a stored reference). Returns a fresh copy or
19
+ * undefined. This function has no Pi runtime dependency; `registry/pi-artifacts/artifact.ts` carries the same validator for
20
+ * the browser, because a copied registry item cannot import this package.
21
+ */
22
+ export function parseArtifact(value) {
23
+ if (!isObject(value) || !hasOnly(value, ['schema', 'version', 'id', 'title', 'type', 'mediaType', 'language', 'target', 'revision', 'ordinal']))
24
+ return undefined;
25
+ const { id, title, type, mediaType, language, target, revision, ordinal } = value;
26
+ if (value['schema'] !== 'boring.artifact' || value['version'] !== 1)
27
+ return undefined;
28
+ if (id !== undefined && (typeof id !== 'string' || !ID.test(id)))
29
+ return undefined;
30
+ if (typeof title !== 'string' || !title.trim() || title.length > 200)
31
+ return undefined;
32
+ if (typeof type !== 'string' || !ARTIFACT_TYPES.includes(type))
33
+ return undefined;
34
+ if (typeof mediaType !== 'string' || !/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i.test(mediaType))
35
+ return undefined;
36
+ if (language !== undefined && (typeof language !== 'string' || !LANGUAGE.test(language)))
37
+ return undefined;
38
+ if (typeof revision !== 'string' || !revision || revision.length > 256)
39
+ return undefined;
40
+ if (ordinal !== undefined && (typeof ordinal !== 'number' || !Number.isSafeInteger(ordinal) || ordinal < 1))
41
+ return undefined;
42
+ if (!isObject(target) || !hasOnly(target, ['resource', 'view']) || !isObject(target['resource']) || !hasOnly(target['resource'], ['providerId', 'path']))
43
+ return undefined;
44
+ const { providerId, path } = target['resource'];
45
+ if (typeof providerId !== 'string' || !providerId || typeof path !== 'string' || !path || path.length > 512)
46
+ return undefined;
47
+ const view = target['view'];
48
+ if (!isObject(view))
49
+ return undefined;
50
+ let parsedView;
51
+ if (view['kind'] === 'published' && hasOnly(view, ['kind']))
52
+ parsedView = { kind: 'published' };
53
+ else if (view['kind'] === 'working' && hasOnly(view, ['kind', 'viewId']) && typeof view['viewId'] === 'string' && view['viewId'])
54
+ parsedView = { kind: 'working', viewId: view['viewId'] };
55
+ else
56
+ return undefined;
57
+ return { schema: 'boring.artifact', version: 1, ...(id === undefined ? {} : { id }), title, type: type, mediaType, ...(language === undefined ? {} : { language: language }),
58
+ target: { resource: { providerId, path }, view: parsedView }, revision, ...(ordinal === undefined ? {} : { ordinal }) };
59
+ }
@@ -1,44 +1,15 @@
1
1
  import type { ToolExecutionApi } from '@earendil-works/pi-durable';
2
2
  import { Type } from '@earendil-works/pi-ai';
3
3
  import type { Context } from '@earendil-works/chord';
4
- import type { ResourceAccess, ResourceLocator } from '@hachej/boring-files';
4
+ import type { ResourceAccess } from '@hachej/boring-files';
5
5
  import type { WorkspaceBinding, WorkspaceResolver } from './workspaces.js';
6
- import type { TaskArtifactDescriptor } from './task-artifacts.js';
7
6
  export { createTaskArtifact, parseTaskArtifact, parseTaskSubject, taskSubjectKey } from './task-artifacts.js';
8
7
  export type { TaskArtifactDescriptor, TaskSubject } from './task-artifacts.js';
9
- export type PresentedArtifact = ArtifactDescriptor | TaskArtifactDescriptor;
10
- /** Parse file or host-task presentation without treating a record as a workspace file. */
11
- export declare function parsePresentedArtifact(value: unknown): PresentedArtifact | undefined;
8
+ export { ARTIFACT_TYPES, createArtifactDescriptor, parseArtifact, parsePresentedArtifact } from './artifact-descriptor.js';
9
+ export type { ArtifactDescriptor, ArtifactDescriptorInput, ArtifactType, PresentedArtifact } from './artifact-descriptor.js';
12
10
  import type { ViewOpenOptions } from './view-open.js';
13
11
  export { answerViewOpen, parseViewOpenAnswer, VIEW_OPEN_TIMEOUT_MS } from './view-open.js';
14
12
  export type { ViewOpenAnswer, ViewOpenOptions, ViewOpenOutcome } from './view-open.js';
15
- export declare const ARTIFACT_TYPES: readonly ["markdown", "html", "svg", "code", "canvas"];
16
- export type ArtifactType = typeof ARTIFACT_TYPES[number];
17
- /**
18
- * What a tool result carries under its `artifact` key. The descriptor of `present` points at a workspace file (`target`), names the
19
- * revision shown and carries no `id` or `ordinal` (both stay optional for hosts that number their own versions).
20
- */
21
- export interface ArtifactDescriptor {
22
- readonly schema: 'boring.artifact';
23
- readonly version: 1;
24
- readonly id?: string;
25
- readonly title: string;
26
- readonly type: ArtifactType;
27
- readonly mediaType: string;
28
- readonly language?: string;
29
- readonly target: ResourceLocator;
30
- readonly revision: string;
31
- readonly ordinal?: number;
32
- }
33
- export type ArtifactDescriptorInput = Omit<ArtifactDescriptor, 'schema' | 'version'>;
34
- /** Build a descriptor, refusing anything `parseArtifact` would refuse. */
35
- export declare function createArtifactDescriptor(input: ArtifactDescriptorInput): ArtifactDescriptor;
36
- /**
37
- * Strict shape check of an untrusted value (a model-visible tool result, a stored reference). Returns a fresh copy or
38
- * undefined. This function has no Pi runtime dependency; `registry/pi-artifacts/artifact.ts` carries the same validator for
39
- * the browser, because a copied registry item cannot import this package.
40
- */
41
- export declare function parseArtifact(value: unknown): ArtifactDescriptor | undefined;
42
13
  export interface PresentToolOptions {
43
14
  /**
44
15
  * The workspace of each call, resolved like Pi's environment (`@hachej/boring-agent/workspaces`), or one binding `{ files, root }`; absent: the
package/dist/artifacts.js CHANGED
@@ -2,10 +2,10 @@ import { defineTool } from '@earendil-works/pi-durable';
2
2
  import { Type } from '@earendil-works/pi-ai';
3
3
  import { workspacePath } from '@hachej/boring-files/file-types';
4
4
  import { asWorkspaceResolver, workspaceFor } from './workspaces.js';
5
- import { createTaskArtifact, parseTaskArtifact, parseTaskSubject } from './task-artifacts.js';
5
+ import { createTaskArtifact, parseTaskSubject } from './task-artifacts.js';
6
6
  export { createTaskArtifact, parseTaskArtifact, parseTaskSubject, taskSubjectKey } from './task-artifacts.js';
7
- /** Parse file or host-task presentation without treating a record as a workspace file. */
8
- export function parsePresentedArtifact(value) { return parseArtifact(value) ?? parseTaskArtifact(value); }
7
+ import { createArtifactDescriptor } from './artifact-descriptor.js';
8
+ export { ARTIFACT_TYPES, createArtifactDescriptor, parseArtifact, parsePresentedArtifact } from './artifact-descriptor.js';
9
9
  import { PRESENT_TOOL, awaitTaskViewOpen, awaitViewOpen } from './view-open.js';
10
10
  export { answerViewOpen, parseViewOpenAnswer, VIEW_OPEN_TIMEOUT_MS } from './view-open.js';
11
11
  /*
@@ -14,62 +14,6 @@ export { answerViewOpen, parseViewOpenAnswer, VIEW_OPEN_TIMEOUT_MS } from './vie
14
14
  * file tools and calls `present(path)`, which keeps the shown revision in the workspace provider's history and returns a
15
15
  * descriptor pointing at the file. There is no store of artifacts and no index. Nothing is executed here.
16
16
  */
17
- export const ARTIFACT_TYPES = ['markdown', 'html', 'svg', 'code', 'canvas'];
18
- const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
19
- const LANGUAGE = /^[A-Za-z0-9+#._-]{1,32}$/;
20
- const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
21
- const hasOnly = (value, keys) => Object.keys(value).every(key => keys.includes(key));
22
- /** Build a descriptor, refusing anything `parseArtifact` would refuse. */
23
- export function createArtifactDescriptor(input) {
24
- const descriptor = { schema: 'boring.artifact', version: 1, ...input };
25
- const parsed = parseArtifact(descriptor);
26
- if (!parsed)
27
- throw new TypeError('Invalid artifact descriptor');
28
- return parsed;
29
- }
30
- /**
31
- * Strict shape check of an untrusted value (a model-visible tool result, a stored reference). Returns a fresh copy or
32
- * undefined. This function has no Pi runtime dependency; `registry/pi-artifacts/artifact.ts` carries the same validator for
33
- * the browser, because a copied registry item cannot import this package.
34
- */
35
- export function parseArtifact(value) {
36
- if (!isObject(value) || !hasOnly(value, ['schema', 'version', 'id', 'title', 'type', 'mediaType', 'language', 'target', 'revision', 'ordinal']))
37
- return undefined;
38
- const { id, title, type, mediaType, language, target, revision, ordinal } = value;
39
- if (value['schema'] !== 'boring.artifact' || value['version'] !== 1)
40
- return undefined;
41
- if (id !== undefined && (typeof id !== 'string' || !ID.test(id)))
42
- return undefined;
43
- if (typeof title !== 'string' || !title.trim() || title.length > 200)
44
- return undefined;
45
- if (typeof type !== 'string' || !ARTIFACT_TYPES.includes(type))
46
- return undefined;
47
- if (typeof mediaType !== 'string' || !/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i.test(mediaType))
48
- return undefined;
49
- if (language !== undefined && (typeof language !== 'string' || !LANGUAGE.test(language)))
50
- return undefined;
51
- if (typeof revision !== 'string' || !revision || revision.length > 256)
52
- return undefined;
53
- if (ordinal !== undefined && (typeof ordinal !== 'number' || !Number.isSafeInteger(ordinal) || ordinal < 1))
54
- return undefined;
55
- if (!isObject(target) || !hasOnly(target, ['resource', 'view']) || !isObject(target['resource']) || !hasOnly(target['resource'], ['providerId', 'path']))
56
- return undefined;
57
- const { providerId, path } = target['resource'];
58
- if (typeof providerId !== 'string' || !providerId || typeof path !== 'string' || !path || path.length > 512)
59
- return undefined;
60
- const view = target['view'];
61
- if (!isObject(view))
62
- return undefined;
63
- let parsedView;
64
- if (view['kind'] === 'published' && hasOnly(view, ['kind']))
65
- parsedView = { kind: 'published' };
66
- else if (view['kind'] === 'working' && hasOnly(view, ['kind', 'viewId']) && typeof view['viewId'] === 'string' && view['viewId'])
67
- parsedView = { kind: 'working', viewId: view['viewId'] };
68
- else
69
- return undefined;
70
- return { schema: 'boring.artifact', version: 1, ...(id === undefined ? {} : { id }), title, type: type, mediaType, ...(language === undefined ? {} : { language: language }),
71
- target: { resource: { providerId, path }, view: parsedView }, revision, ...(ordinal === undefined ? {} : { ordinal }) };
72
- }
73
17
  const reply = (value) => ({ output: [{ type: 'text', text: JSON.stringify(value) }] });
74
18
  const decode = (bytes) => { try {
75
19
  return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
package/dist/room.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { Conversation, ConversationId, EntryRecord, Harness } from '@earendil-works/pi-durable';
2
2
  import type { Context } from '@earendil-works/chord';
3
3
  import type { AskUserAnswer } from './ask-user.js';
4
+ import type { PresentedArtifact } from './artifact-descriptor.js';
5
+ export type { PresentedArtifact } from './artifact-descriptor.js';
4
6
  /**
5
7
  * A shared room on pi-durable: people and agents in one session that never runs a model. Every entry is a passive write
6
8
  * keyed by a namespaced `requestId` (CH-20); `roomView` is the only reader (CH-24); `wakeFor` alone decides which agent
@@ -41,6 +43,7 @@ export type RoomEntryData = {
41
43
  attachments: {
42
44
  path: string;
43
45
  }[];
46
+ artifacts?: PresentedArtifact[];
44
47
  steps: RoomStep[];
45
48
  };
46
49
  /** The target stays stored and reads as « message supprimé » (CH-6). */
@@ -66,6 +69,7 @@ export type RoomEntryData = {
66
69
  prompt: string;
67
70
  options: string[];
68
71
  allowFreeText: boolean;
72
+ artifacts?: PresentedArtifact[];
69
73
  };
70
74
  'room.action': {
71
75
  author: string;
@@ -109,6 +113,7 @@ export type RoomItem = {
109
113
  attachments: {
110
114
  path: string;
111
115
  }[];
116
+ artifacts?: PresentedArtifact[];
112
117
  steps: RoomStep[];
113
118
  mine: boolean;
114
119
  removed: boolean;
@@ -135,6 +140,7 @@ export type RoomItem = {
135
140
  options: string[];
136
141
  allowFreeText: boolean;
137
142
  answered: boolean;
143
+ artifacts?: PresentedArtifact[];
138
144
  } | {
139
145
  kind: 'system';
140
146
  id: number;
@@ -218,12 +224,14 @@ export type RoomRefusal = {
218
224
  export type RoomReply = {
219
225
  readonly text: string;
220
226
  readonly steps?: readonly RoomStep[];
227
+ readonly artifacts?: readonly PresentedArtifact[];
221
228
  };
222
229
  export type RoomQuestionDraft = {
223
230
  readonly callId: string;
224
231
  readonly prompt: string;
225
232
  readonly options: readonly string[];
226
233
  readonly allowFreeText: boolean;
234
+ readonly artifacts?: readonly PresentedArtifact[];
227
235
  };
228
236
  export interface Room {
229
237
  /** The native conversation of a room conversation name, created on first use. */
@@ -261,6 +269,7 @@ export interface PendingQuestion {
261
269
  readonly prompt: string;
262
270
  readonly options: readonly string[];
263
271
  readonly allowFreeText: boolean;
272
+ readonly artifacts?: readonly PresentedArtifact[];
264
273
  }
265
274
  /** `ask_user` calls of the active transcript that have no tool result yet, oldest first. */
266
275
  export declare function pendingQuestions(entries: readonly EntryRecord[]): PendingQuestion[];
@@ -272,6 +281,9 @@ export type RoomFrame = {
272
281
  readonly type: 'step';
273
282
  readonly label: string;
274
283
  readonly status: 'running' | 'done';
284
+ } | {
285
+ readonly type: 'artifact';
286
+ readonly artifact: PresentedArtifact;
275
287
  };
276
288
  /** The agent's door into the room, bound to the agent by its host: the agent id is never an argument. */
277
289
  export interface RoomPort {
package/dist/room.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { defineDoc } from '@earendil-works/pi-durable';
2
2
  import { withAbortSignal } from '@earendil-works/chord/context';
3
3
  import { ASK_USER_TOOL, answerUserQuestion, askUserQuestionId } from './ask-user.js';
4
+ import { parsePresentedArtifact } from './artifact-descriptor.js';
4
5
  /**
5
6
  * A shared room on pi-durable: people and agents in one session that never runs a model. Every entry is a passive write
6
7
  * keyed by a namespaced `requestId` (CH-20); `roomView` is the only reader (CH-24); `wakeFor` alone decides which agent
@@ -51,6 +52,7 @@ export function roomView(entries, cards, viewer) {
51
52
  items.push({
52
53
  kind: 'message', id, author: data.author, ...(data.via ? { via: data.via } : {}), ...(data.for ? { for: data.for } : {}),
53
54
  text: gone ? ROOM.removed : data.text, mentions: [...data.mentions], ...(data.replyTo !== undefined ? { replyTo: data.replyTo } : {}),
55
+ ...(data.artifacts ? { artifacts: gone ? [] : descriptors(data.artifacts) } : {}),
54
56
  attachments: gone ? [] : data.attachments.map(({ path }) => ({ path })), steps: gone ? [] : data.steps.map(({ label, status }) => ({ label, status })),
55
57
  mine: data.author === viewer.id || data.via === viewer.id, removed: gone,
56
58
  });
@@ -64,7 +66,7 @@ export function roomView(entries, cards, viewer) {
64
66
  items.push({ kind: 'pin', id, author: data.author, title: data.title, line: data.line, anchor: data.anchor, mine: data.author === viewer.id });
65
67
  }
66
68
  else if (kind === 'room.question') {
67
- items.push({ kind: 'question', id, author: data.author, callId: data.callId, prompt: data.prompt, options: [...data.options], allowFreeText: data.allowFreeText, answered: answered.has(id) });
69
+ items.push({ kind: 'question', id, author: data.author, callId: data.callId, prompt: data.prompt, options: [...data.options], allowFreeText: data.allowFreeText, answered: answered.has(id), ...(data.artifacts ? { artifacts: descriptors(data.artifacts) } : {}) });
68
70
  }
69
71
  else if (kind === 'room.system') {
70
72
  items.push({ kind: 'system', id, line: data.line });
@@ -185,11 +187,11 @@ export function createRoom(options) {
185
187
  }),
186
188
  settle: (agent, wakeId, reply) => onWake(agent, wakeId, {
187
189
  requestId: `wake:${wakeId}`, kind: 'room.message',
188
- data: { author: agent, text: reply.text, mentions: [], attachments: [], steps: (reply.steps ?? []).map(({ label, status }) => ({ label, status })) },
190
+ data: { author: agent, text: reply.text, mentions: [], attachments: [], ...(reply.artifacts ? { artifacts: descriptors(reply.artifacts) } : {}), steps: (reply.steps ?? []).map(({ label, status }) => ({ label, status })) },
189
191
  }, true),
190
192
  question: (agent, wakeId, question) => onWake(agent, wakeId, {
191
193
  requestId: `q:${question.callId}`, kind: 'room.question',
192
- data: { author: agent, callId: question.callId, prompt: question.prompt, options: [...question.options], allowFreeText: question.allowFreeText },
194
+ data: { author: agent, callId: question.callId, prompt: question.prompt, options: [...question.options], allowFreeText: question.allowFreeText, ...(question.artifacts ? { artifacts: descriptors(question.artifacts) } : {}) },
193
195
  }, false),
194
196
  resend: async () => {
195
197
  const at = now();
@@ -257,6 +259,40 @@ function answerText(entry) {
257
259
  return (entry?.model ?? []).flatMap(message => message.role === 'assistant' ? message.content : [])
258
260
  .flatMap(part => part.type === 'text' ? [part.text] : []).join('\n\n').trim();
259
261
  }
262
+ /** Exact resource/view/revision identity, not a path guessed from an assistant's prose. */
263
+ const descriptorKey = (artifact) => JSON.stringify(artifact.type === 'task' ? artifact.subject : [artifact.target, artifact.revision]);
264
+ const descriptors = (values) => {
265
+ const unique = new Map();
266
+ for (const value of values) {
267
+ const artifact = parsePresentedArtifact(value);
268
+ if (artifact)
269
+ unique.set(descriptorKey(artifact), artifact);
270
+ }
271
+ return [...unique.values()];
272
+ };
273
+ /** Only successful native present results paired with their actual invocation may project descriptors. */
274
+ function presented(entries) {
275
+ const calls = new Map();
276
+ const found = [];
277
+ for (const entry of entries)
278
+ for (const message of entry.model ?? []) {
279
+ if (message.role === 'assistant')
280
+ for (const part of message.content) {
281
+ if (part.type === 'toolCall')
282
+ calls.set(part.id, part.name);
283
+ }
284
+ if (message.role !== 'toolResult' || message.isError || message.toolName !== 'present' || calls.get(message.toolCallId) !== 'present')
285
+ continue;
286
+ try {
287
+ const value = JSON.parse(message.content.flatMap(part => part.type === 'text' ? [part.text] : []).join(''));
288
+ const artifact = value && typeof value === 'object' ? parsePresentedArtifact(value['artifact']) : undefined;
289
+ if (artifact)
290
+ found.push(artifact);
291
+ }
292
+ catch { /* A failed or malformed present has no descriptor, only its ordinary step. */ }
293
+ }
294
+ return descriptors(found);
295
+ }
260
296
  const framesOf = (entries) => entries.flatMap(entry => (entry.model ?? []).flatMap((message) => {
261
297
  if (message.role === 'toolResult')
262
298
  return [{ type: 'step', label: message.toolName, status: 'done' }];
@@ -299,6 +335,7 @@ export function createRoomTurn(options) {
299
335
  async function follow(wakeId, known) {
300
336
  const watch = await conversation.watch(context);
301
337
  const sent = new Map();
338
+ const sentArtifacts = new Set();
302
339
  let from = known;
303
340
  let submission;
304
341
  let live = '';
@@ -340,6 +377,14 @@ export function createRoomTurn(options) {
340
377
  live = text;
341
378
  }
342
379
  }
380
+ const artifacts = presented(own);
381
+ for (const artifact of artifacts) {
382
+ const key = descriptorKey(artifact);
383
+ if (!sentArtifacts.has(key)) {
384
+ frames.push({ type: 'artifact', artifact });
385
+ sentArtifacts.add(key);
386
+ }
387
+ }
343
388
  const questions = pendingQuestions(own);
344
389
  if (frames.length && port.stream)
345
390
  await port.stream(wakeId, frames);
@@ -347,7 +392,7 @@ export function createRoomTurn(options) {
347
392
  return;
348
393
  const asked = (await owed(wakeId))?.asked ?? [];
349
394
  for (const question of questions.filter(question => !asked.includes(question.callId))) {
350
- await port.question(wakeId, question);
395
+ await port.question(wakeId, { ...question, ...(artifacts.length ? { artifacts } : {}) });
351
396
  // Asking cannot share the native commit: a crash before this acknowledgement repeats it, and `q:<callId>` absorbs that.
352
397
  await update(wakeId, (items, index) => { if (!items[index].asked.includes(question.callId))
353
398
  items[index].asked.push(question.callId); });
@@ -381,7 +426,15 @@ export function createRoomTurn(options) {
381
426
  // An unanswered run is closed with the room's own line, never dropped: left open it would only wait for its resends.
382
427
  if (record.type === 'input' && record.status === 'done') {
383
428
  const entry = (await conversation.entries({ minEntryId: record.answer, maxEntryId: record.answer }, 1, undefined, context)).items[0];
384
- await port.settle(wakeId, { text: answerText(entry) });
429
+ const entries = [];
430
+ let cursor;
431
+ do {
432
+ const page = await conversation.entries({ minEntryId: (record.entry + 1), maxEntryId: record.answer, order: 'ascending' }, 200, cursor, context);
433
+ entries.push(...page.items);
434
+ cursor = page.next;
435
+ } while (cursor !== undefined);
436
+ const artifacts = presented(entries);
437
+ await port.settle(wakeId, { text: answerText(entry), ...(artifacts.length ? { artifacts } : {}) });
385
438
  }
386
439
  else if (record.type === 'input' && record.status === 'unanswered')
387
440
  await port.settle(wakeId, { text: ROOM.unanswered });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-agent",
3
- "version": "0.4.37",
3
+ "version": "0.4.39",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "type": "module",
@@ -188,8 +188,8 @@
188
188
  "@earendil-works/pi-durable": "~1.4.0",
189
189
  "@earendil-works/pi-ai": "~1.4.0",
190
190
  "@earendil-works/chord": "~1.4.0",
191
- "@hachej/boring-files": "^0.4.37",
192
- "@hachej/boring-ui-kit": "^0.4.37",
191
+ "@hachej/boring-files": "^0.4.39",
192
+ "@hachej/boring-ui-kit": "^0.4.39",
193
193
  "typebox": "1.3.27",
194
194
  "xlsx": "^0.20.3"
195
195
  },