@yeaft/webchat-agent 1.0.613 → 1.0.615
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/local-runtime/server/handlers/client-person.js +4 -2
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +206 -174
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/person/capabilities.js +14 -5
- package/yeaft/person/contracts.js +29 -2
- package/yeaft/person/inspection.js +30 -4
- package/yeaft/person/sqlite-store.js +10 -3
- package/yeaft/person/task-host.js +48 -6
|
Binary file
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import { NATIVE_TOOL_MANIFESTS, NATIVE_TOOL_IDS, isNativeTool } from './native-t
|
|
|
2
2
|
import { bytes, digest, fail, identifier, object, page, text } from './contracts.js';
|
|
3
3
|
import { createdCapabilityRecord, validateCreatedDefinition } from './created-capability-contract.js';
|
|
4
4
|
import { runPersonScript, scriptInput, testPersonScript } from './script-executor.js';
|
|
5
|
-
import { createdSkillView, inspectionPage } from './inspection.js';
|
|
5
|
+
import { chronologicalCursor, createdSkillView, inspectionCursor, inspectionPage, inspectionTime } from './inspection.js';
|
|
6
6
|
|
|
7
7
|
// Intrinsic cognition, bounded pure scripts and supported native host tools.
|
|
8
8
|
// No shell/filesystem/network/VP authority is exposed to generated code.
|
|
@@ -83,10 +83,19 @@ export function inspectCapabilities(created, { cursor, limit }, nativeToolIds =
|
|
|
83
83
|
contract.evidence = item.evidence;
|
|
84
84
|
entries.push(item);
|
|
85
85
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
const boundary = inspectionCursor(cursor, 'skills');
|
|
87
|
+
// IDs are ASCII identifiers. Binary ordering matches SQLite. Only custom
|
|
88
|
+
// versions have creation dates; builtins follow as an undated ID catalogue.
|
|
89
|
+
const byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
90
|
+
if (boundary?.legacy) return inspectionPage(entries.filter(entry => entry.id > boundary.id).sort(byId), limit, 'id');
|
|
91
|
+
const custom = entries.filter(entry => entry.source.kind === 'person-created')
|
|
92
|
+
.filter(entry => !boundary || (boundary.stage === 'c' && (inspectionTime(entry.createdAt) < boundary.time ||
|
|
93
|
+
(inspectionTime(entry.createdAt) === boundary.time && entry.id > boundary.id))))
|
|
94
|
+
.sort((a, b) => inspectionTime(b.createdAt) - inspectionTime(a.createdAt) || byId(a, b));
|
|
95
|
+
const builtin = entries.filter(entry => entry.source.kind !== 'person-created')
|
|
96
|
+
.filter(entry => boundary?.stage !== 'b' || entry.id > boundary.id).sort(byId);
|
|
97
|
+
return inspectionPage([...custom, ...builtin], limit,
|
|
98
|
+
record => chronologicalCursor('skills', record, record.source.kind === 'person-created' ? 'c' : 'b'));
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
export class PersonCapabilities {
|
|
@@ -66,12 +66,39 @@ export function page(payload = {}) {
|
|
|
66
66
|
}
|
|
67
67
|
/** Task APIs are explicit owner actions, not cognitive admissions. Logs are raw
|
|
68
68
|
* owner-scoped output; list/control responses never include native runtime/log/result.
|
|
69
|
-
*
|
|
69
|
+
* Empty list payloads retain the legacy active-first capped inventory. Explicit
|
|
70
|
+
* {limit,cursor?} pages each collection newest-created first, with immutable
|
|
71
|
+
* creation-time/ID keysets and one scope-bound continuation for both collections.
|
|
72
|
+
* Paged responses also carry a bounded active control snapshot independent of
|
|
73
|
+
* history position (including terminal children with pending actual execution).
|
|
70
74
|
* Log offsets count raw UTF-8 bytes; an omitted offset starts at the beginning.
|
|
71
75
|
*/
|
|
72
76
|
export const PERSON_TASK_LIMITS = Object.freeze({ records: 100, logBytes: 16384, maxLogBytes: 65536 });
|
|
77
|
+
export function taskInspectionCursor(cursor) {
|
|
78
|
+
if (typeof cursor !== 'string' || !/^t1:[A-Za-z0-9_-]{1,2048}$/.test(cursor)) fail('INVALID_REQUEST');
|
|
79
|
+
let value;
|
|
80
|
+
try { value = JSON.parse(Buffer.from(cursor.slice(3), 'base64url').toString('utf8')); } catch { fail('INVALID_REQUEST'); }
|
|
81
|
+
object(value, ['scope', 'tasks', 'agents']);
|
|
82
|
+
if (typeof value.scope !== 'string' || !/^[a-f0-9]{64}$/.test(value.scope)) fail('INVALID_REQUEST');
|
|
83
|
+
for (const key of ['tasks', 'agents']) {
|
|
84
|
+
const boundary = value[key];
|
|
85
|
+
if (boundary === null) continue;
|
|
86
|
+
object(boundary, ['time', 'id']);
|
|
87
|
+
if (!Number.isSafeInteger(boundary.time) || boundary.time < 0 || typeof boundary.id !== 'string' ||
|
|
88
|
+
!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(boundary.id)) fail('INVALID_REQUEST');
|
|
89
|
+
}
|
|
90
|
+
if ((value.tasks === null && value.agents === null) || Buffer.from(JSON.stringify(value)).toString('base64url') !== cursor.slice(3)) fail('INVALID_REQUEST');
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
73
93
|
export function personTaskRequest(op, payload = {}) {
|
|
74
|
-
if (op === 'tasks') {
|
|
94
|
+
if (op === 'tasks') {
|
|
95
|
+
object(payload, ['cursor', 'limit'], []);
|
|
96
|
+
if (!Object.keys(payload).length) return {};
|
|
97
|
+
const limit = payload.limit === undefined ? PERSON_TASK_LIMITS.records : payload.limit, cursor = payload.cursor ?? null;
|
|
98
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > PERSON_TASK_LIMITS.records) fail('INVALID_REQUEST');
|
|
99
|
+
if (cursor !== null) taskInspectionCursor(cursor);
|
|
100
|
+
return { cursor, limit };
|
|
101
|
+
}
|
|
75
102
|
const idKey = op === 'agent_close' ? 'agentId' : 'taskId';
|
|
76
103
|
object(payload, op === 'task_log' ? ['taskId', 'offset', 'maxBytes'] : [idKey], [idKey]);
|
|
77
104
|
// Native TaskStore normalizes punctuation. Reject aliases instead of letting
|
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
import { bytes, fail, identifier, object, page, text } from './contracts.js';
|
|
2
2
|
|
|
3
|
+
/** Timestamp + binary ID keysets survive insertion, deletion of the boundary
|
|
4
|
+
* record and same-millisecond writes. Builtins have no invented chronology.
|
|
5
|
+
* Bare ID cursors remain readable for pre-keyset clients' ID-ascending pages. */
|
|
6
|
+
export function inspectionCursor(value, section) {
|
|
7
|
+
if (value == null) return null;
|
|
8
|
+
if (typeof value !== 'string' || value.length > 512) fail('INVALID_REQUEST');
|
|
9
|
+
const prefix = section === 'memory' ? 'm1' : 's1';
|
|
10
|
+
const match = /^(m1|s1):(c|b):(\d{1,16}):([A-Za-z0-9_-]+)$/.exec(value);
|
|
11
|
+
if (!match) {
|
|
12
|
+
if (/^(m1|s1):/.test(value)) fail('INVALID_REQUEST');
|
|
13
|
+
identifier(value); return { legacy: true, id: value };
|
|
14
|
+
}
|
|
15
|
+
const [, type, stage, rawTime, encoded] = match;
|
|
16
|
+
const time = Number(rawTime), id = Buffer.from(encoded, 'base64url').toString('utf8');
|
|
17
|
+
identifier(id);
|
|
18
|
+
if (type !== prefix || (section === 'memory' && stage !== 'c') || !Number.isSafeInteger(time) || time < 0 ||
|
|
19
|
+
String(time) !== rawTime || (stage === 'b' && time !== 0) || Buffer.from(id).toString('base64url') !== encoded) fail('INVALID_REQUEST');
|
|
20
|
+
return { stage, time, id };
|
|
21
|
+
}
|
|
22
|
+
export const inspectionTime = value => {
|
|
23
|
+
const time = value == null ? NaN : new Date(value).getTime();
|
|
24
|
+
return Number.isSafeInteger(time) && time >= 0 ? time : 0;
|
|
25
|
+
};
|
|
26
|
+
export const chronologicalCursor = (section, record, stage = 'c') =>
|
|
27
|
+
`${section === 'memory' ? 'm1' : 's1'}:${stage}:${stage === 'b' ? 0 : inspectionTime(section === 'memory' ? record.updatedAt : record.createdAt)}:${Buffer.from(record.id).toString('base64url')}`;
|
|
28
|
+
|
|
3
29
|
/** Public read APIs use keyset cursors, never offsets or model-ranked recall. */
|
|
4
30
|
export function inspectRequest(payload = {}) {
|
|
5
31
|
object(payload, ['section', 'cursor', 'limit'], ['section']);
|
|
6
32
|
if (!['memory', 'skills'].includes(payload.section)) fail('INVALID_REQUEST');
|
|
7
33
|
const { limit } = page({ limit: payload.limit });
|
|
8
34
|
const cursor = payload.cursor ?? null;
|
|
9
|
-
|
|
35
|
+
inspectionCursor(cursor, payload.section);
|
|
10
36
|
return { section: payload.section, cursor, limit };
|
|
11
37
|
}
|
|
12
38
|
export function searchRequest(payload = {}) {
|
|
@@ -48,13 +74,13 @@ export function createdSkillView(record, contract) {
|
|
|
48
74
|
/** Complete records only. Continuation follows the last returned key even when the
|
|
49
75
|
* byte budget, rather than the requested count, ends a page. No attachment bytes. */
|
|
50
76
|
export function inspectionPage(records, limit, key, view = value => value) {
|
|
51
|
-
const items = []; let size = 0;
|
|
77
|
+
const items = []; let size = 0, lastRecord;
|
|
52
78
|
for (const record of records) {
|
|
53
79
|
if (items.length === limit) break;
|
|
54
80
|
const item = view(record), cost = bytes(item);
|
|
55
81
|
if (cost > 256 * 1024) fail('OUTPUT_LIMIT');
|
|
56
82
|
if (items.length && size + cost > 256 * 1024) break;
|
|
57
|
-
items.push(item); size += cost;
|
|
83
|
+
items.push(item); size += cost; lastRecord = record;
|
|
58
84
|
}
|
|
59
|
-
return { items, nextCursor: records.length > items.length ? String(items.at(-1)[key]) : null };
|
|
85
|
+
return { items, nextCursor: records.length > items.length ? (typeof key === 'function' ? key(lastRecord) : String(items.at(-1)[key])) : null };
|
|
60
86
|
}
|
|
@@ -8,7 +8,7 @@ import { CREATED_CAPABILITY_LIMITS, createdCapabilityRecord, validateCreatedCapa
|
|
|
8
8
|
import { SCHEMA, TABLES } from './sqlite-schema.js';
|
|
9
9
|
import { attachmentMetadata, attachmentRequestHash, validateFiles } from './attachments.js';
|
|
10
10
|
import { capabilityExperienceView, recordCapabilityExperience } from './capability-experience.js';
|
|
11
|
-
import { conceptView, inspectionPage, inspectRequest, messageView, personName, searchRequest, settingsView, stateView } from './inspection.js';
|
|
11
|
+
import { chronologicalCursor, conceptView, inspectionCursor, inspectionPage, inspectRequest, messageView, personName, searchRequest, settingsView, stateView } from './inspection.js';
|
|
12
12
|
import { TOKEN_FIELDS, turnsPage, turnView } from './turn-diagnostics.js';
|
|
13
13
|
import { inspectCapabilities } from './capabilities.js';
|
|
14
14
|
|
|
@@ -476,8 +476,15 @@ export class SqlitePersonStore {
|
|
|
476
476
|
this.getPerson(ownerId);
|
|
477
477
|
const scope = this.scope(ownerId);
|
|
478
478
|
if (section === 'skills') return inspectCapabilities(this.rows('created_capabilities', scope, ' ORDER BY id ASC LIMIT 32'), { cursor, limit }, nativeToolIds);
|
|
479
|
-
const
|
|
480
|
-
|
|
479
|
+
const boundary = inspectionCursor(cursor, section);
|
|
480
|
+
if (boundary?.legacy) {
|
|
481
|
+
const records = this.rows('concepts', scope, ' AND id > ? ORDER BY id ASC LIMIT ?', [boundary.id, limit + 1]);
|
|
482
|
+
return inspectionPage(records, limit, 'id', conceptView);
|
|
483
|
+
}
|
|
484
|
+
const records = this.rows('concepts', scope,
|
|
485
|
+
' AND (? IS NULL OR updatedAt < ? OR (updatedAt = ? AND id > ?)) ORDER BY updatedAt DESC, id ASC LIMIT ?',
|
|
486
|
+
[boundary?.time ?? null, boundary?.time ?? null, boundary?.time ?? null, boundary?.id ?? null, limit + 1]);
|
|
487
|
+
return inspectionPage(records, limit, record => chronologicalCursor(section, record), conceptView);
|
|
481
488
|
}
|
|
482
489
|
search(ownerId, options) {
|
|
483
490
|
const { query, cursor, limit } = searchRequest(options);
|
|
@@ -13,7 +13,8 @@ import { isTerminalAgentStatus } from '../sub-agent/status.js';
|
|
|
13
13
|
import { describeAgentOutcome } from '../sub-agent/outcome.js';
|
|
14
14
|
import { NullTrace } from '../debug-trace.js';
|
|
15
15
|
import { loadConfig } from '../config.js';
|
|
16
|
-
import { digest, fail, PERSON_TASK_LIMITS, personTaskRequest } from './contracts.js';
|
|
16
|
+
import { digest, fail, PERSON_TASK_LIMITS, personTaskRequest, taskInspectionCursor } from './contracts.js';
|
|
17
|
+
import { inspectionPage, inspectionTime } from './inspection.js';
|
|
17
18
|
|
|
18
19
|
// Deliberately not the full Session registry: no transcript search, routing,
|
|
19
20
|
// interactive UI, MCP, Work Center, or nested orchestration authority.
|
|
@@ -77,13 +78,13 @@ function projectedTask(task) {
|
|
|
77
78
|
sourceEpisodeId: task.source?.episodeId ?? null, agentId: task.runtime?.subAgentId ?? null,
|
|
78
79
|
recoveryStatus: task.status === 'orphaned' ? 'orphaned' : null };
|
|
79
80
|
}
|
|
80
|
-
function projectedAgent(agent, executionPending) {
|
|
81
|
+
function projectedAgent(agent, executionPending, taskId) {
|
|
81
82
|
const usage = Object.fromEntries(['tokens', 'turns', 'startedAt'].map(key => [key,
|
|
82
83
|
Number.isFinite(agent.usage?.[key]) && agent.usage[key] >= 0 ? agent.usage[key] : 0]));
|
|
83
84
|
// Only the helper's fixed outcome vocabulary crosses this boundary, never the
|
|
84
85
|
// native result, budget reason, final report, or partial output.
|
|
85
86
|
const { status, complete, reason, truncated } = describeAgentOutcome(agent);
|
|
86
|
-
return { id: agent.id, name: summary(agent.name, 160), status: agent.status,
|
|
87
|
+
return { id: agent.id, taskId, name: summary(agent.name, 160), status: agent.status,
|
|
87
88
|
outcome: { status, complete, reason, truncated }, executionPending,
|
|
88
89
|
mission: summary(agent.mission ?? agent.task, 1024), usage, createdAt: agent.createdAt ?? null,
|
|
89
90
|
sourceEpisodeId: agent.personEpisodeId ?? null, recoveryStatus: agent.recoveryStatus === 'orphaned' ? 'orphaned' : null };
|
|
@@ -440,6 +441,17 @@ class ScopedPersonTaskHost {
|
|
|
440
441
|
return task;
|
|
441
442
|
}
|
|
442
443
|
|
|
444
|
+
#projectAgent(agent, executionPending) {
|
|
445
|
+
// The persistent link is independent of either history page. Only canonical
|
|
446
|
+
// IDs of this Person's actual child tasks may become owner log controls.
|
|
447
|
+
const id = agent.taskId;
|
|
448
|
+
const task = typeof id === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(id)
|
|
449
|
+
? this.taskManager.getTask(this.sessionId, id, this.parentVpId) : null;
|
|
450
|
+
const taskId = task && task.id === id && task.sessionId === this.sessionId && task.kind === 'sub_agent'
|
|
451
|
+
&& task.runtime?.subAgentId === agent.id ? id : null;
|
|
452
|
+
return projectedAgent(agent, executionPending, taskId);
|
|
453
|
+
}
|
|
454
|
+
|
|
443
455
|
/** Owner-facing inspection/control: never attach a provider, schedule cognition
|
|
444
456
|
* or cancel unrelated work. Native snapshots remain private to evidence(). */
|
|
445
457
|
async request({ ownerId, personId, namespace = this.scope.namespace, op, payload = {} } = {}) {
|
|
@@ -448,16 +460,46 @@ class ScopedPersonTaskHost {
|
|
|
448
460
|
const args = personTaskRequest(op, payload);
|
|
449
461
|
switch (op) {
|
|
450
462
|
case 'tasks': {
|
|
463
|
+
const continuation = args.cursor ? taskInspectionCursor(args.cursor) : null;
|
|
464
|
+
if (continuation && continuation.scope !== this.scope.key) throw denied();
|
|
451
465
|
this.#persistAgents();
|
|
452
466
|
const tasks = jsonFiles(this.#manager.store.sessionDir(this.sessionId))
|
|
453
467
|
.filter(task => task.sessionId === this.sessionId && task.ownerVpId === this.parentVpId && ['shell', 'sub_agent'].includes(task.kind));
|
|
454
468
|
const agents = [...this.#agents.values()].filter(agent => agentBelongsToScope(agent, this.agentScope));
|
|
469
|
+
const byCreation = (a, b) => inspectionTime(b.createdAt) - inspectionTime(a.createdAt) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
470
|
+
if (args.limit !== undefined) {
|
|
471
|
+
const readPage = (items, key, view) => {
|
|
472
|
+
const boundary = continuation?.[key];
|
|
473
|
+
if (continuation && boundary === null) return { items: [], nextCursor: null };
|
|
474
|
+
const records = items.filter(item => !boundary || inspectionTime(item.createdAt) < boundary.time ||
|
|
475
|
+
(inspectionTime(item.createdAt) === boundary.time && item.id > boundary.id))
|
|
476
|
+
.sort(byCreation)
|
|
477
|
+
.slice(0, args.limit + 1);
|
|
478
|
+
return inspectionPage(records, args.limit, item => ({ time: inspectionTime(item.createdAt), id: item.id }), view);
|
|
479
|
+
};
|
|
480
|
+
const taskPage = readPage(tasks, 'tasks', projectedTask);
|
|
481
|
+
const agentPage = readPage(agents, 'agents', agent => this.#projectAgent(agent, agent.executionPending));
|
|
482
|
+
const more = taskPage.nextCursor !== null || agentPage.nextCursor !== null;
|
|
483
|
+
const nextCursor = more ? `t1:${Buffer.from(JSON.stringify({ scope: this.scope.key,
|
|
484
|
+
tasks: taskPage.nextCursor, agents: agentPage.nextCursor })).toString('base64url')}` : null;
|
|
485
|
+
// History stays strictly newest-created first. A separate, bounded
|
|
486
|
+
// control snapshot keeps older live shells and terminal children with
|
|
487
|
+
// detached tools reachable without scrolling through settled history.
|
|
488
|
+
const activePage = (items, isActive, view) => inspectionPage(items.filter(isActive).sort(byCreation),
|
|
489
|
+
PERSON_TASK_LIMITS.records, 'id', view);
|
|
490
|
+
const activeTasks = activePage(tasks, task => !isTerminalTaskStatus(task.status), projectedTask);
|
|
491
|
+
const activeAgents = activePage(agents, agent => !isTerminalAgentStatus(agent.status) || agent.executionPending,
|
|
492
|
+
agent => this.#projectAgent(agent, agent.executionPending));
|
|
493
|
+
return { tasks: taskPage.items, agents: agentPage.items, nextCursor, truncated: more,
|
|
494
|
+
active: { tasks: activeTasks.items, agents: activeAgents.items,
|
|
495
|
+
truncated: activeTasks.nextCursor !== null || activeAgents.nextCursor !== null } };
|
|
496
|
+
}
|
|
455
497
|
const recent = (items, settled) => items.sort((a, b) => Number(settled(a)) - Number(settled(b))
|
|
456
|
-
||
|
|
498
|
+
|| byCreation(a, b)).slice(0, PERSON_TASK_LIMITS.records);
|
|
457
499
|
// A terminal driver with actual tools still running is active work, not
|
|
458
500
|
// disposable history: keep its cleanup control ahead of settled records.
|
|
459
501
|
return { tasks: recent(tasks, task => isTerminalTaskStatus(task.status)).map(projectedTask),
|
|
460
|
-
agents: recent(agents, agent => isTerminalAgentStatus(agent.status) && !agent.executionPending).map(agent =>
|
|
502
|
+
agents: recent(agents, agent => isTerminalAgentStatus(agent.status) && !agent.executionPending).map(agent => this.#projectAgent(agent, agent.executionPending)),
|
|
461
503
|
truncated: tasks.length > PERSON_TASK_LIMITS.records || agents.length > PERSON_TASK_LIMITS.records };
|
|
462
504
|
}
|
|
463
505
|
case 'task_log': {
|
|
@@ -483,7 +525,7 @@ class ScopedPersonTaskHost {
|
|
|
483
525
|
parentEngineDeps: this.agentScope, taskManager: this.taskManager }));
|
|
484
526
|
if (!result.success) fail('TASK_CONTROL_UNAVAILABLE');
|
|
485
527
|
await this.#joinAgent(agent);
|
|
486
|
-
return { agent:
|
|
528
|
+
return { agent: this.#projectAgent(agent, this.#executionPending(agent)), pending: false };
|
|
487
529
|
}
|
|
488
530
|
default: fail('INVALID_REQUEST');
|
|
489
531
|
}
|