@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.
- package/dist/artifact-descriptor.d.ts +33 -0
- package/dist/artifact-descriptor.js +59 -0
- package/dist/artifacts.d.ts +3 -32
- package/dist/artifacts.js +3 -59
- package/dist/room.d.ts +12 -0
- package/dist/room.js +58 -5
- package/package.json +3 -3
|
@@ -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
|
+
}
|
package/dist/artifacts.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
10
|
-
|
|
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,
|
|
5
|
+
import { createTaskArtifact, parseTaskSubject } from './task-artifacts.js';
|
|
6
6
|
export { createTaskArtifact, parseTaskArtifact, parseTaskSubject, taskSubjectKey } from './task-artifacts.js';
|
|
7
|
-
|
|
8
|
-
export
|
|
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
|
-
|
|
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.
|
|
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.
|
|
192
|
-
"@hachej/boring-ui-kit": "^0.4.
|
|
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
|
},
|