@animalabs/agent-framework 0.6.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/agent.d.ts +6 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +11 -1
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +129 -15
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +967 -113
- package/dist/src/framework.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +87 -7
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +329 -49
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +10 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -6
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +2 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +17 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +143 -7
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/types/agent.d.ts +32 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +32 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/channel-registry-routing.test.js +186 -10
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/explicit-prose-routing.test.d.ts +11 -0
- package/dist/test/explicit-prose-routing.test.d.ts.map +1 -0
- package/dist/test/explicit-prose-routing.test.js +247 -0
- package/dist/test/explicit-prose-routing.test.js.map +1 -0
- package/dist/test/present-while-acting.test.js +85 -8
- package/dist/test/present-while-acting.test.js.map +1 -1
- package/dist/test/trunk-channel-routing.test.js +7 -3
- package/dist/test/trunk-channel-routing.test.js.map +1 -1
- package/dist/test/workspace-watcher.test.js +161 -9
- package/dist/test/workspace-watcher.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/agent.ts +11 -1
- package/src/framework.ts +1037 -116
- package/src/mcpl/channel-registry.ts +401 -54
- package/src/modules/workspace/index.ts +60 -6
- package/src/modules/workspace/types.ts +2 -0
- package/src/modules/workspace/watcher.ts +160 -7
- package/src/types/agent.ts +35 -0
- package/src/types/trace.ts +47 -1
|
@@ -651,6 +651,15 @@ export class WorkspaceModule implements Module {
|
|
|
651
651
|
error: err.message,
|
|
652
652
|
} as ProcessEvent);
|
|
653
653
|
},
|
|
654
|
+
onReattach: () => {
|
|
655
|
+
mount.watcherError = null;
|
|
656
|
+
void this.initialScan(name);
|
|
657
|
+
this.ctx?.pushEvent({
|
|
658
|
+
type: 'workspace:watcher-reattached',
|
|
659
|
+
mount: name,
|
|
660
|
+
path: mount.config.path,
|
|
661
|
+
} as ProcessEvent);
|
|
662
|
+
},
|
|
654
663
|
},
|
|
655
664
|
);
|
|
656
665
|
watcher.start();
|
|
@@ -724,13 +733,13 @@ export class WorkspaceModule implements Module {
|
|
|
724
733
|
return [
|
|
725
734
|
{
|
|
726
735
|
name: 'read',
|
|
727
|
-
description: 'Read a file from the workspace. Returns content with line numbers.',
|
|
736
|
+
description: 'Read a file from the workspace. Returns content with line numbers. Reads at most 2000 lines per call by default — use offset/limit to page through larger files.',
|
|
728
737
|
inputSchema: {
|
|
729
738
|
type: 'object' as const,
|
|
730
739
|
properties: {
|
|
731
740
|
path: { type: 'string', description: 'File path (mount-prefixed, e.g., "project/src/main.ts")' },
|
|
732
741
|
offset: { type: 'number', description: 'Starting line number (1-indexed)' },
|
|
733
|
-
limit: { type: 'number', description: 'Maximum number of lines to return' },
|
|
742
|
+
limit: { type: 'number', description: 'Maximum number of lines to return (default 2000)' },
|
|
734
743
|
},
|
|
735
744
|
required: ['path'],
|
|
736
745
|
},
|
|
@@ -1018,6 +1027,30 @@ export class WorkspaceModule implements Module {
|
|
|
1018
1027
|
};
|
|
1019
1028
|
}
|
|
1020
1029
|
|
|
1030
|
+
/**
|
|
1031
|
+
* Read a file's raw bytes from a mount (through the Chronicle tree, synced
|
|
1032
|
+
* first like the `read` tool). Public API for the framework's synthesized
|
|
1033
|
+
* `read_image` tool and other peer callers that need binary content.
|
|
1034
|
+
*/
|
|
1035
|
+
async readBinary(
|
|
1036
|
+
mountPrefixedPath: string,
|
|
1037
|
+
): Promise<{ data: Buffer } | { error: string }> {
|
|
1038
|
+
let mount: MountState;
|
|
1039
|
+
let relativePath: string;
|
|
1040
|
+
try {
|
|
1041
|
+
({ mount, relativePath } = this.parsePath(mountPrefixedPath));
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
1044
|
+
}
|
|
1045
|
+
const store = this.getStore();
|
|
1046
|
+
await this.ensureSynced(mount, relativePath);
|
|
1047
|
+
const entry = store.treeGet(mount.treeStateId, relativePath);
|
|
1048
|
+
if (!entry) return { error: `File not found: ${mountPrefixedPath}` };
|
|
1049
|
+
const blob = store.getBlob(entry.blobHash);
|
|
1050
|
+
if (!blob) return { error: `Blob not found for: ${mountPrefixedPath}` };
|
|
1051
|
+
return { data: blob };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1021
1054
|
/**
|
|
1022
1055
|
* Parse a mount-prefixed path into (mountName, relativePath).
|
|
1023
1056
|
*/
|
|
@@ -1311,10 +1344,16 @@ export class WorkspaceModule implements Module {
|
|
|
1311
1344
|
const content = blob.toString('utf-8');
|
|
1312
1345
|
const lines = content.split('\n');
|
|
1313
1346
|
|
|
1314
|
-
// Apply offset/limit
|
|
1347
|
+
// Apply offset/limit. An unlimited read of a large file would inject the
|
|
1348
|
+
// whole thing into the turn (a 2MB file ≈ 600k tokens → the request blows
|
|
1349
|
+
// past the model context and 400s), so an omitted limit falls back to a
|
|
1350
|
+
// default cap; the result reports total/from/to so the agent can page.
|
|
1351
|
+
const DEFAULT_READ_LINE_LIMIT = 2000;
|
|
1315
1352
|
const startLine = (input.offset ?? 1) - 1; // Convert to 0-indexed
|
|
1316
|
-
const
|
|
1353
|
+
const effectiveLimit = input.limit ?? DEFAULT_READ_LINE_LIMIT;
|
|
1354
|
+
const endLine = startLine + effectiveLimit;
|
|
1317
1355
|
const slice = lines.slice(startLine, endLine);
|
|
1356
|
+
const truncated = endLine < lines.length;
|
|
1318
1357
|
|
|
1319
1358
|
// Format with line numbers (cat -n style)
|
|
1320
1359
|
const formatted = slice
|
|
@@ -1328,6 +1367,9 @@ export class WorkspaceModule implements Module {
|
|
|
1328
1367
|
totalLines: lines.length,
|
|
1329
1368
|
fromLine: startLine + 1,
|
|
1330
1369
|
toLine: Math.min(endLine, lines.length),
|
|
1370
|
+
...(truncated
|
|
1371
|
+
? { note: `Truncated at ${effectiveLimit} lines (file has ${lines.length}). Use offset/limit to read more.` }
|
|
1372
|
+
: {}),
|
|
1331
1373
|
content: formatted,
|
|
1332
1374
|
},
|
|
1333
1375
|
};
|
|
@@ -1662,8 +1704,20 @@ export class WorkspaceModule implements Module {
|
|
|
1662
1704
|
mount.initialSyncDone = true;
|
|
1663
1705
|
}
|
|
1664
1706
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1707
|
+
// If `path` points at a single FILE, grep just that file. Otherwise treat
|
|
1708
|
+
// `path` as a directory prefix (the original behaviour). Previously a file
|
|
1709
|
+
// path produced an empty prefix like "notes.md/" and matched nothing, so
|
|
1710
|
+
// grepping a specific file silently returned zero results.
|
|
1711
|
+
let entries: Array<{ path: string; blobHash: string }>;
|
|
1712
|
+
const fileNode = relativePath
|
|
1713
|
+
? (store.treeGet(mount.treeStateId, relativePath) as { blobHash?: string } | null)
|
|
1714
|
+
: null;
|
|
1715
|
+
if (fileNode && fileNode.blobHash) {
|
|
1716
|
+
entries = [{ path: relativePath, blobHash: fileNode.blobHash }];
|
|
1717
|
+
} else {
|
|
1718
|
+
const prefix = relativePath ? relativePath + '/' : undefined;
|
|
1719
|
+
entries = store.treeList(mount.treeStateId, prefix);
|
|
1720
|
+
}
|
|
1667
1721
|
|
|
1668
1722
|
for (const entry of entries) {
|
|
1669
1723
|
if (fileGlob && !fileGlob.test(entry.path)) continue;
|
|
@@ -25,6 +25,8 @@ export interface MountConfig {
|
|
|
25
25
|
watch?: 'always' | 'on-agent-action' | 'never';
|
|
26
26
|
/** Debounce window in ms for watch: 'always' mode (default: 300) */
|
|
27
27
|
watchDebounceMs?: number;
|
|
28
|
+
/** Poll interval in ms for checking watched root identity changes (default: 2000) */
|
|
29
|
+
watchRootPollMs?: number;
|
|
28
30
|
/**
|
|
29
31
|
* Simple ignore patterns (NOT full gitignore syntax). Supported forms:
|
|
30
32
|
* - Exact name match: ".git", "node_modules"
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { watch, type FSWatcher } from 'chokidar';
|
|
9
|
-
import { mkdirSync } from 'node:fs';
|
|
9
|
+
import { mkdirSync, statSync } from 'node:fs';
|
|
10
|
+
import { basename } from 'node:path';
|
|
10
11
|
import { type MountConfig } from './types.js';
|
|
11
12
|
|
|
12
13
|
export type FsOp = 'created' | 'modified' | 'deleted';
|
|
@@ -30,6 +31,14 @@ export interface WatcherLifecycle {
|
|
|
30
31
|
onReady?: () => void;
|
|
31
32
|
/** Fires on any chokidar-reported error (ENOENT, EACCES, platform faults). */
|
|
32
33
|
onError?: (err: Error) => void;
|
|
34
|
+
/** Fires after the watcher re-attaches to a replaced root inode. */
|
|
35
|
+
onReattach?: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface RootIdentity {
|
|
39
|
+
dev: number;
|
|
40
|
+
ino: number;
|
|
41
|
+
birthtimeMs: number;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
/**
|
|
@@ -42,8 +51,13 @@ export class MountWatcher {
|
|
|
42
51
|
// create -> modify collapses to 'created' (single batch); delete always wins.
|
|
43
52
|
private pendingChanges = new Map<string, FsOp>();
|
|
44
53
|
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
54
|
+
private rootPollTimer: ReturnType<typeof setInterval> | null = null;
|
|
45
55
|
private suppressedPaths = new Set<string>();
|
|
46
56
|
private suppressionTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
57
|
+
private rootIdentity: RootIdentity | null = null;
|
|
58
|
+
private detached = false;
|
|
59
|
+
private attaching = false;
|
|
60
|
+
private stopped = true;
|
|
47
61
|
private readonly debounceMs: number;
|
|
48
62
|
private readonly config: MountConfig;
|
|
49
63
|
private readonly onChangeCallback: (changes: FsChange[]) => void;
|
|
@@ -64,8 +78,14 @@ export class MountWatcher {
|
|
|
64
78
|
* Start watching the filesystem.
|
|
65
79
|
*/
|
|
66
80
|
start(): void {
|
|
67
|
-
if (this.watcher) return;
|
|
81
|
+
if (this.watcher || this.rootPollTimer) return;
|
|
82
|
+
|
|
83
|
+
this.stopped = false;
|
|
84
|
+
this.attach();
|
|
85
|
+
this.startRootPolling();
|
|
86
|
+
}
|
|
68
87
|
|
|
88
|
+
private attach(): void {
|
|
69
89
|
// Read-write mounts: ensure the path exists before chokidar attaches.
|
|
70
90
|
// chokidar 4's fs.watch-based backend silently fails to fire events when
|
|
71
91
|
// the watched path OR any of its parent directories don't exist at
|
|
@@ -110,21 +130,39 @@ export class MountWatcher {
|
|
|
110
130
|
this.watcher.on('add', handleEvent('created'));
|
|
111
131
|
this.watcher.on('change', handleEvent('modified'));
|
|
112
132
|
this.watcher.on('unlink', handleEvent('deleted'));
|
|
133
|
+
this.watcher.on('unlinkDir', (dirPath) => {
|
|
134
|
+
if (this.isRootPath(dirPath)) {
|
|
135
|
+
this.detached = true;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
113
138
|
|
|
114
139
|
if (this.lifecycle.onReady) {
|
|
115
140
|
this.watcher.once('ready', this.lifecycle.onReady);
|
|
116
141
|
}
|
|
117
|
-
|
|
118
|
-
this.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
this.watcher.on('error', (err) => {
|
|
143
|
+
this.reportError(err);
|
|
144
|
+
});
|
|
145
|
+
this.watcher.on('raw', (event: string, rawPath: string, _details: unknown) => {
|
|
146
|
+
if (event !== 'rename') return;
|
|
147
|
+
// A rename on the watched root's path/basename is the one signal that
|
|
148
|
+
// survives inode reuse on Linux, where unlinkDir/error never fire.
|
|
149
|
+
if (this.isRootPath(rawPath) || basename(rawPath) === basename(this.config.path)) {
|
|
150
|
+
void this.checkRootLiveness({ force: true });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
this.captureRootIdentity();
|
|
122
155
|
}
|
|
123
156
|
|
|
124
157
|
/**
|
|
125
158
|
* Stop watching.
|
|
126
159
|
*/
|
|
127
160
|
async stop(): Promise<void> {
|
|
161
|
+
this.stopped = true;
|
|
162
|
+
if (this.rootPollTimer) {
|
|
163
|
+
clearInterval(this.rootPollTimer);
|
|
164
|
+
this.rootPollTimer = null;
|
|
165
|
+
}
|
|
128
166
|
if (this.debounceTimer) {
|
|
129
167
|
clearTimeout(this.debounceTimer);
|
|
130
168
|
this.debounceTimer = null;
|
|
@@ -140,6 +178,9 @@ export class MountWatcher {
|
|
|
140
178
|
await this.watcher.close();
|
|
141
179
|
this.watcher = null;
|
|
142
180
|
}
|
|
181
|
+
this.rootIdentity = null;
|
|
182
|
+
this.detached = false;
|
|
183
|
+
this.attaching = false;
|
|
143
184
|
}
|
|
144
185
|
|
|
145
186
|
/**
|
|
@@ -219,6 +260,108 @@ export class MountWatcher {
|
|
|
219
260
|
}, this.debounceMs);
|
|
220
261
|
}
|
|
221
262
|
|
|
263
|
+
private startRootPolling(): void {
|
|
264
|
+
const pollMs = this.config.watchRootPollMs ?? 2000;
|
|
265
|
+
this.rootPollTimer = setInterval(() => {
|
|
266
|
+
void this.checkRootLiveness();
|
|
267
|
+
}, pollMs);
|
|
268
|
+
this.rootPollTimer.unref();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private async checkRootLiveness(opts: { force?: boolean } = {}): Promise<void> {
|
|
272
|
+
if (this.stopped || this.attaching) return;
|
|
273
|
+
|
|
274
|
+
let current: RootIdentity;
|
|
275
|
+
try {
|
|
276
|
+
current = this.readRootIdentity();
|
|
277
|
+
} catch (err) {
|
|
278
|
+
if (this.isErrno(err, 'ENOENT')) {
|
|
279
|
+
this.detached = true;
|
|
280
|
+
if (this.config.mode === 'read-write') {
|
|
281
|
+
await this.reattach();
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
this.reportError(err);
|
|
286
|
+
if (opts.force) {
|
|
287
|
+
this.detached = true;
|
|
288
|
+
await this.reattach();
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!this.rootIdentity && !this.detached) {
|
|
294
|
+
await this.reattach();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!opts.force && !this.detached && this.rootIdentity
|
|
299
|
+
&& this.sameIdentity(current, this.rootIdentity)) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await this.reattach();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private async reattach(): Promise<void> {
|
|
307
|
+
if (this.attaching || this.stopped) return;
|
|
308
|
+
|
|
309
|
+
this.attaching = true;
|
|
310
|
+
try {
|
|
311
|
+
const oldWatcher = this.watcher;
|
|
312
|
+
this.watcher = null;
|
|
313
|
+
if (oldWatcher) {
|
|
314
|
+
await oldWatcher.close();
|
|
315
|
+
}
|
|
316
|
+
if (this.stopped) return;
|
|
317
|
+
|
|
318
|
+
this.attach();
|
|
319
|
+
if (!this.detached && this.rootIdentity) {
|
|
320
|
+
this.lifecycle.onReattach?.();
|
|
321
|
+
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
this.reportError(err);
|
|
324
|
+
} finally {
|
|
325
|
+
this.attaching = false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private captureRootIdentity(): void {
|
|
330
|
+
try {
|
|
331
|
+
this.rootIdentity = this.readRootIdentity();
|
|
332
|
+
this.detached = false;
|
|
333
|
+
} catch (err) {
|
|
334
|
+
if (this.isErrno(err, 'ENOENT')) {
|
|
335
|
+
this.rootIdentity = null;
|
|
336
|
+
this.detached = true;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
this.rootIdentity = null;
|
|
340
|
+
this.detached = false;
|
|
341
|
+
this.reportError(err);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private readRootIdentity(): RootIdentity {
|
|
346
|
+
const stat = statSync(this.config.path);
|
|
347
|
+
return { dev: stat.dev, ino: stat.ino, birthtimeMs: stat.birthtimeMs };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private sameIdentity(a: RootIdentity, b: RootIdentity): boolean {
|
|
351
|
+
// birthtimeMs is only a secondary tripwire: sub-millisecond replacements
|
|
352
|
+
// can reuse both the inode and timestamp, so raw rename remains essential.
|
|
353
|
+
return a.dev === b.dev && a.ino === b.ino && a.birthtimeMs === b.birthtimeMs;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private isErrno(err: unknown, code: string): boolean {
|
|
357
|
+
return typeof err === 'object' && err !== null && 'code' in err
|
|
358
|
+
&& (err as NodeJS.ErrnoException).code === code;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private reportError(err: unknown): void {
|
|
362
|
+
this.lifecycle.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
363
|
+
}
|
|
364
|
+
|
|
222
365
|
private toRelative(absolutePath: string): string | null {
|
|
223
366
|
const base = this.config.path.endsWith('/')
|
|
224
367
|
? this.config.path
|
|
@@ -228,4 +371,14 @@ export class MountWatcher {
|
|
|
228
371
|
}
|
|
229
372
|
return null;
|
|
230
373
|
}
|
|
374
|
+
|
|
375
|
+
private isRootPath(absolutePath: string): boolean {
|
|
376
|
+
const root = this.config.path.endsWith('/')
|
|
377
|
+
? this.config.path.slice(0, -1)
|
|
378
|
+
: this.config.path;
|
|
379
|
+
const candidate = absolutePath.endsWith('/')
|
|
380
|
+
? absolutePath.slice(0, -1)
|
|
381
|
+
: absolutePath;
|
|
382
|
+
return candidate === root;
|
|
383
|
+
}
|
|
231
384
|
}
|
package/src/types/agent.ts
CHANGED
|
@@ -68,6 +68,23 @@ export interface AgentConfig {
|
|
|
68
68
|
*/
|
|
69
69
|
cacheTtl?: '5m' | '1h';
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Whether to emit prompt-cache markers (cache_control) on requests.
|
|
73
|
+
* Default true (Anthropic API). Set false for transports that reject
|
|
74
|
+
* cache_control — e.g. Bedrock legacy Claude models, which fail with
|
|
75
|
+
* "your request did not allow prompt caching".
|
|
76
|
+
*/
|
|
77
|
+
promptCaching?: boolean;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Prefill-formatter support: a synthetic user message appended after the
|
|
81
|
+
* conversation (e.g. chapterx's `<cmd>cat untitled.txt</cmd>` CLI-sim
|
|
82
|
+
* scaffold). Forwarded verbatim to membrane's prefill formatters; ignored
|
|
83
|
+
* by native formatting. Part of reproducing a prefill-era bot's exact
|
|
84
|
+
* prompting structure when migrating it into a resident.
|
|
85
|
+
*/
|
|
86
|
+
prefillUserMessage?: string;
|
|
87
|
+
|
|
71
88
|
/** Provider-specific request parameters (for example Responses reasoning
|
|
72
89
|
* and server-side compaction settings). */
|
|
73
90
|
providerParams?: Record<string, unknown>;
|
|
@@ -116,6 +133,16 @@ export interface AgentConfig {
|
|
|
116
133
|
*/
|
|
117
134
|
announceHumanTurns?: boolean;
|
|
118
135
|
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* How the agent's PLAIN PROSE (non-tool output) reaches channels.
|
|
139
|
+
* - 'locus' (default): host-inferred — the turn-frozen locus machinery.
|
|
140
|
+
* - 'explicit': the model prefixes prose with a destination
|
|
141
|
+
* (`>>#channel` / `>>@person` / `>>skip_reply`); unprefixed prose is never
|
|
142
|
+
* delivered — it bounces to a clipboard for a cheap prefixed resend.
|
|
143
|
+
* See docs/explicit-prose-routing.md.
|
|
144
|
+
*/
|
|
145
|
+
proseRouting?: 'locus' | 'explicit';
|
|
119
146
|
}
|
|
120
147
|
|
|
121
148
|
/** The intentionally small set of agent settings that may change in-process. */
|
|
@@ -222,4 +249,12 @@ export interface InferenceRequest {
|
|
|
222
249
|
* which correctly fall back to the global default channel.
|
|
223
250
|
*/
|
|
224
251
|
channelId?: string;
|
|
252
|
+
/**
|
|
253
|
+
* True when the triggering message explicitly addressed the agent (mention,
|
|
254
|
+
* reply-to-bot, DM — `chat:addressed` in MCPL RFC-001 terms). When a wake
|
|
255
|
+
* batch spans several channels, an addressed channel outranks ambient
|
|
256
|
+
* chatter for the turn's frozen speech locus: overheard conversation must
|
|
257
|
+
* not capture the agent's voice just by being newest.
|
|
258
|
+
*/
|
|
259
|
+
addressed?: boolean;
|
|
225
260
|
}
|
package/src/types/trace.ts
CHANGED
|
@@ -37,7 +37,18 @@ export type TraceEvent =
|
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
// Inference lifecycle
|
|
40
|
-
| (TraceEventBase & {
|
|
40
|
+
| (TraceEventBase & {
|
|
41
|
+
type: 'inference:started';
|
|
42
|
+
agentName: string;
|
|
43
|
+
/**
|
|
44
|
+
* The turn-frozen outbound locus (see turnLocusPins) — the channel this
|
|
45
|
+
* turn's plain prose will be routed to. Omitted for turns with no locus
|
|
46
|
+
* (heartbeats with no default channel). Observability only, like every
|
|
47
|
+
* trace field; lets external taps (e.g. a TTS relay) tag the whole
|
|
48
|
+
* activation with its channel without re-deriving routing.
|
|
49
|
+
*/
|
|
50
|
+
channelId?: string;
|
|
51
|
+
})
|
|
41
52
|
| (TraceEventBase & {
|
|
42
53
|
type: 'inference:completed';
|
|
43
54
|
agentName: string;
|
|
@@ -82,6 +93,8 @@ export type TraceEvent =
|
|
|
82
93
|
blockType: 'text' | 'thinking' | 'tool_call' | 'tool_result';
|
|
83
94
|
/** 0-indexed block position in the current assistant turn. */
|
|
84
95
|
blockIndex: number;
|
|
96
|
+
/** Turn-frozen outbound locus for this turn (see inference:started). */
|
|
97
|
+
channelId?: string;
|
|
85
98
|
})
|
|
86
99
|
| (TraceEventBase & {
|
|
87
100
|
/**
|
|
@@ -95,6 +108,8 @@ export type TraceEvent =
|
|
|
95
108
|
phase: 'block_start' | 'block_complete';
|
|
96
109
|
blockType: 'text' | 'thinking' | 'tool_call' | 'tool_result';
|
|
97
110
|
blockIndex: number;
|
|
111
|
+
/** Turn-frozen outbound locus for this turn (see inference:started). */
|
|
112
|
+
channelId?: string;
|
|
98
113
|
})
|
|
99
114
|
| (TraceEventBase & {
|
|
100
115
|
type: 'inference:tool_calls_yielded';
|
|
@@ -281,6 +296,37 @@ export type TraceEvent =
|
|
|
281
296
|
channelId?: string;
|
|
282
297
|
})
|
|
283
298
|
|
|
299
|
+
// Host-owned speech routing (ChannelRegistry.routeSpeech). One trace per
|
|
300
|
+
// delivered prose segment. `text` is the segment verbatim — the same content
|
|
301
|
+
// already on the bus chunk-by-chunk via inference:tokens, and mcpl:* traces
|
|
302
|
+
// are 'ops'-scoped on the webui wire — and `messageId` is the surface's id
|
|
303
|
+
// for the posted message (when the MCPL server reports one). Together they
|
|
304
|
+
// give external taps the (channelId, messageId, text) tuple needed to edit
|
|
305
|
+
// a just-posted message, e.g. a TTS interruption truncating a reply to the
|
|
306
|
+
// words actually voiced.
|
|
307
|
+
| (TraceEventBase & {
|
|
308
|
+
type: 'mcpl:speech-routed';
|
|
309
|
+
/** Agent / conversation whose speech was routed. */
|
|
310
|
+
conversationId: string;
|
|
311
|
+
serverId: string;
|
|
312
|
+
channelId: string;
|
|
313
|
+
delivered: boolean;
|
|
314
|
+
textLen: number;
|
|
315
|
+
/** The routed prose segment, verbatim. */
|
|
316
|
+
text: string;
|
|
317
|
+
/** Surface message id (e.g. Discord message id), when reported. */
|
|
318
|
+
messageId?: string;
|
|
319
|
+
})
|
|
320
|
+
| (TraceEventBase & {
|
|
321
|
+
type: 'mcpl:speech-route-failed';
|
|
322
|
+
/** Agent / conversation whose speech failed to route. */
|
|
323
|
+
conversationId: string;
|
|
324
|
+
/** Empty string when the failure was "no locus at all". */
|
|
325
|
+
channelId: string;
|
|
326
|
+
reason: string;
|
|
327
|
+
textLen: number;
|
|
328
|
+
})
|
|
329
|
+
|
|
284
330
|
// MCPL server connection lifecycle (spawn / handshake / reconnect health).
|
|
285
331
|
// Previously these outcomes were visible only on the host process's own
|
|
286
332
|
// stderr, so a server that lost the boot handshake race just went missing.
|