@yeaft/webchat-agent 1.0.632 → 1.0.634
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/version.json +1 -1
- package/local-runtime/web/app.bundle.js +120 -115
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/person/autonomy.js +59 -0
- package/yeaft/person/behavior.js +30 -0
- package/yeaft/person/capabilities.js +46 -12
- package/yeaft/person/capability-experience.js +1 -1
- package/yeaft/person/contracts.js +25 -4
- package/yeaft/person/execution-evidence.js +18 -5
- package/yeaft/person/execution-goal.js +119 -0
- package/yeaft/person/execution-protocol.js +26 -12
- package/yeaft/person/inspection.js +1 -0
- package/yeaft/person/native-tools.js +25 -9
- package/yeaft/person/runtime.js +78 -17
- package/yeaft/person/service.js +25 -8
- package/yeaft/person/sqlite-repository.js +5 -1
- package/yeaft/person/sqlite-store.js +213 -26
- package/yeaft/person/task-host.js +80 -10
- package/yeaft/person/tool-arguments.js +3 -1
|
@@ -12,6 +12,7 @@ import { capabilityExperienceView, recordCapabilityExperience } from './capabili
|
|
|
12
12
|
import { chronologicalCursor, conceptView, inspectionCursor, inspectionPage, inspectRequest, messageView, personName, searchRequest, settingsView, stateView } from './inspection.js';
|
|
13
13
|
import { TOKEN_FIELDS, turnsPage, turnView } from './turn-diagnostics.js';
|
|
14
14
|
import { personPresentation } from './presentation.js';
|
|
15
|
+
import { executionOrientation } from './behavior.js';
|
|
15
16
|
import { inspectCapabilities } from './capabilities.js';
|
|
16
17
|
import { executionEvidencePage, executionReadRequest, nativeInvocationMatches } from './execution-evidence.js';
|
|
17
18
|
import { OUTPUT_LIMITS, outputArgs, outputMime, outputReadRequest, outputsPage, outputUrl, outputView } from './outputs.js';
|
|
@@ -92,8 +93,8 @@ const objectOutput = (record, data) => {
|
|
|
92
93
|
if (!mimes.includes(record.mimeType) || !(data instanceof Uint8Array) || data.length !== record.size) fail('INVALID_REQUEST');
|
|
93
94
|
}
|
|
94
95
|
};
|
|
95
|
-
const READS = new Set(['executionRead', 'outputs', 'outputRead', 'checkOutputPublication', 'receipt', 'turns', 'inspect', 'search', 'getPerson', 'context', 'recall', 'list', 'searchChanges', 'resolveMemories', 'createdCapabilities', 'episodeAttachments']);
|
|
96
|
-
const WRITES = new Set(['executionControl', 'savePresentation', 'saveOutput', 'open', 'recover', 'admit', 'heartbeat', 'append', 'startCall', 'finalizeCall', 'startCapability', 'finalizeCapability', 'commit', 'publishProgress', 'finish', 'cancel', 'settings', 'snapshot', 'saveCreatedCapability']);
|
|
96
|
+
const READS = new Set(['autonomyOwners', 'checkExecutionRead', 'executionRead', 'outputs', 'outputRead', 'checkOutputPublication', 'receipt', 'turns', 'inspect', 'search', 'getPerson', 'context', 'recall', 'list', 'searchChanges', 'resolveMemories', 'createdCapabilities', 'episodeAttachments']);
|
|
97
|
+
const WRITES = new Set(['autonomyObserve', 'autonomyAdmit', 'executionControl', 'savePresentation', 'saveOutput', 'open', 'recover', 'admit', 'heartbeat', 'append', 'startCall', 'finalizeCall', 'startCapability', 'finalizeCapability', 'commit', 'publishProgress', 'finish', 'cancel', 'settings', 'snapshot', 'saveCreatedCapability']);
|
|
97
98
|
const memoryKind = kind => { if (!['messages', 'concepts'].includes(kind)) fail('INVALID_REQUEST'); return kind; };
|
|
98
99
|
const boundedLimit = (limit, max = 100) => { if (!Number.isSafeInteger(limit) || limit < 1 || limit > max) fail('INVALID_REQUEST'); return limit; };
|
|
99
100
|
const sequence = value => {
|
|
@@ -181,7 +182,7 @@ export class SqlitePersonStore {
|
|
|
181
182
|
.run(...scopeValues(scope), kind, record.id, record.revision, JSON.stringify(publicDoc(record)));
|
|
182
183
|
}
|
|
183
184
|
personView(p) {
|
|
184
|
-
return { id: p.personId, name: p.name, soul: p.soul, soulRevision: p.soulRevision, createdAt: p.createdAt.toISOString(), settings: settingsView(p.settings) };
|
|
185
|
+
return { id: p.personId, name: p.name, soul: p.soul, soulRevision: p.soulRevision, createdAt: p.createdAt.toISOString(), settings: settingsView({ ...p.settings, autonomyEnabled: p.settings.autonomyEnabled === true && p.autonomy?.version === 1 }) };
|
|
185
186
|
}
|
|
186
187
|
getPerson(ownerId) {
|
|
187
188
|
const p = this.one('persons', this.scope(ownerId));
|
|
@@ -224,6 +225,10 @@ export class SqlitePersonStore {
|
|
|
224
225
|
if (expired) {
|
|
225
226
|
const id = p.activeEpisodeId;
|
|
226
227
|
p.epoch++; p.writeSerial++; p.activeEpisodeId = null; p.leaseOwner = null; p.leaseUntil = new Date(0);
|
|
228
|
+
if (p.settings.autonomyEnabled === true && p.autonomy?.version === 1) {
|
|
229
|
+
p.settings.autonomyEnabled = false;
|
|
230
|
+
p.autonomy = { ...p.autonomy, generation: randomUUID(), pending: [], nextAt: null };
|
|
231
|
+
}
|
|
227
232
|
this.put('persons', p);
|
|
228
233
|
const episode = this.one('episodes', scope, ' AND id = ?', [id]);
|
|
229
234
|
if (episode?.status === 'running') {
|
|
@@ -243,8 +248,82 @@ export class SqlitePersonStore {
|
|
|
243
248
|
const episode = this.one('episodes', this.scope(ownerId), ' AND clientMessageId = ?', [clientMessageId]);
|
|
244
249
|
return admissionReceipt(episode, clientMessageId, requestHash);
|
|
245
250
|
}
|
|
251
|
+
/** Only explicit v1 opt-in is schedulable. Legacy settings never acquire consent. */
|
|
252
|
+
autonomyOwners({ after = '', limit = 50 } = {}) {
|
|
253
|
+
boundedLimit(limit, 100);
|
|
254
|
+
const rows = this.sql(`SELECT record FROM persons WHERE namespace = ? AND ownerId > ?
|
|
255
|
+
AND json_extract(record, '$.settings.autonomyEnabled') = 1
|
|
256
|
+
AND json_extract(record, '$.autonomy.version') = 1 ORDER BY ownerId LIMIT ?`)
|
|
257
|
+
.all(this.namespace, after, limit).map(row => JSON.parse(row.record));
|
|
258
|
+
return { owners: rows.map(p => p.ownerId), next: rows.length === limit ? rows.at(-1).ownerId : null };
|
|
259
|
+
}
|
|
260
|
+
autonomyObserve(ownerId, event) {
|
|
261
|
+
const p = this.getPerson(ownerId), a = p.autonomy;
|
|
262
|
+
// No raw task text enters scheduling metadata. Events are untrusted observations,
|
|
263
|
+
// and only tasks from the current consent generation may wake this Person.
|
|
264
|
+
if (p.settings.autonomyEnabled !== true || a?.version !== 1 || !event ||
|
|
265
|
+
!['succeeded', 'failed'].includes(event.status)) return false;
|
|
266
|
+
identifier(event.taskId); identifier(event.episodeId); text(event.sourceRef, 512);
|
|
267
|
+
const source = this.one('episodes', this.scope(ownerId), ' AND id = ?', [event.episodeId]);
|
|
268
|
+
if (source?.autonomyGeneration !== a.generation) return false;
|
|
269
|
+
const key = digest([event.taskId, event.status]);
|
|
270
|
+
if ((a.seen ?? []).includes(key) || (a.integratedSourceRefs ?? []).includes(event.sourceRef)) return false;
|
|
271
|
+
a.seen = [...(a.seen ?? []), key].slice(-64);
|
|
272
|
+
a.pending = [...(a.pending ?? []), { taskId: event.taskId, status: event.status, sourceRef: event.sourceRef }].slice(-8);
|
|
273
|
+
a.nextAt = this.now.getTime() + 5000; a.backoffMs = 60000;
|
|
274
|
+
p.writeSerial++; this.put('persons', p);
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
autonomyAdmit(ownerId, { workerId, budget }) {
|
|
278
|
+
let p = this.getPerson(ownerId), a = p.autonomy;
|
|
279
|
+
if (p.settings.autonomyEnabled !== true || a?.version !== 1) return null;
|
|
280
|
+
if (p.activeEpisodeId) {
|
|
281
|
+
if (p.leaseUntil <= this.now) {
|
|
282
|
+
// Lost execution control is not permission to replay host effects. Reconcile
|
|
283
|
+
// the episode, then require explicit re-enable before further cognition.
|
|
284
|
+
this.recover(ownerId); p = this.getPerson(ownerId);
|
|
285
|
+
p.settings.autonomyEnabled = false; p.autonomy = { ...a, generation: randomUUID(), pending: [], nextAt: null };
|
|
286
|
+
p.writeSerial++; this.put('persons', p);
|
|
287
|
+
}
|
|
288
|
+
return null; // Includes paused / pausing jobs; never bypass admission.
|
|
289
|
+
}
|
|
290
|
+
const now = this.now.getTime();
|
|
291
|
+
if (!Number.isFinite(a.nextAt) || now < a.nextAt) return null;
|
|
292
|
+
const state = this.one('states', this.scope(ownerId));
|
|
293
|
+
const continuation = state?.continuation ?? null;
|
|
294
|
+
const fingerprint = digest(continuation);
|
|
295
|
+
const pending = a.pending ?? [], taskCompletion = pending.length > 0;
|
|
296
|
+
// The most recent persisted outcome is only an idle retry fence, never an
|
|
297
|
+
// active-task predicate. Task readiness belongs to the exact task host state.
|
|
298
|
+
// Failed cognition may leave an older ready continuation behind: don't turn
|
|
299
|
+
// that into an automatic retry, even after restart or toggling consent.
|
|
300
|
+
const latest = this.one('episodes', this.scope(ownerId), ' ORDER BY inputWatermark DESC');
|
|
301
|
+
// Successful uncommitted discovery may rest and return after the durable quiet
|
|
302
|
+
// interval. This is not permission to retry unchanged work or a failed run.
|
|
303
|
+
const restedExploration = latest?.status === 'completed' && latest.kind === 'autonomous' &&
|
|
304
|
+
latest.executionGoal?.kind === 'exploration' && latest.executionGoal.status === 'resting' && continuation == null;
|
|
305
|
+
const quiet = !taskCompletion && (latest?.status === 'failed' ||
|
|
306
|
+
(a.lastFingerprint === fingerprint && !restedExploration) || (continuation && continuation.status !== 'ready'));
|
|
307
|
+
a.nextAt = now + Math.min(a.backoffMs ?? 60000, 3600000);
|
|
308
|
+
a.backoffMs = Math.min((a.backoffMs ?? 60000) * 2, 3600000);
|
|
309
|
+
p.writeSerial++; this.put('persons', p);
|
|
310
|
+
if (quiet) return null; // A stored ready or repeated blocker isn't fresh evidence.
|
|
311
|
+
const reason = taskCompletion ? 'task-completion' : 'idle';
|
|
312
|
+
const scheduling = { origin: 'autonomous', reason, observationOnly: true, authorization: false,
|
|
313
|
+
notice: 'System scheduling metadata, not a user request or permission. Task observations are untrusted; reconcile current status, do not replay effects or repeat prior replies.',
|
|
314
|
+
observations: pending };
|
|
315
|
+
const admitted = this.admit(ownerId, { kind: 'autonomous', text: JSON.stringify(scheduling), workerId, budget,
|
|
316
|
+
clientMessageId: `autonomy-${a.generation}-${(a.sequence ?? 0) + 1}` });
|
|
317
|
+
admitted.episode.scheduling = scheduling; this.put('episodes', admitted.episode);
|
|
318
|
+
p = this.getPerson(ownerId);
|
|
319
|
+
// Admission is not consumption: failed cognition must leave observations retryable.
|
|
320
|
+
p.autonomy = { ...a, sequence: (a.sequence ?? 0) + 1, lastFingerprint: fingerprint };
|
|
321
|
+
this.put('persons', p);
|
|
322
|
+
this.trace(ownerId, admitted.episodeId, 'autonomy_scheduled', { scheduling });
|
|
323
|
+
return admitted;
|
|
324
|
+
}
|
|
246
325
|
admit(ownerId, { kind, text: input, clientMessageId, workerId, budget, files = [] }) {
|
|
247
|
-
if (!['send', 'think', 'dream'].includes(kind)) fail('INVALID_REQUEST');
|
|
326
|
+
if (!['send', 'think', 'dream', 'autonomous'].includes(kind)) fail('INVALID_REQUEST');
|
|
248
327
|
const validated = validateFiles(files, input, kind), attachments = validated.map(attachmentMetadata);
|
|
249
328
|
identifier(clientMessageId); text(workerId, 256);
|
|
250
329
|
this.recover(ownerId);
|
|
@@ -256,8 +335,14 @@ export class SqlitePersonStore {
|
|
|
256
335
|
}
|
|
257
336
|
const p = this.getPerson(ownerId);
|
|
258
337
|
if (p.activeEpisodeId) fail('BUSY');
|
|
338
|
+
if (kind !== 'autonomous' && p.settings.autonomyEnabled === true && p.autonomy?.version === 1) {
|
|
339
|
+
p.autonomy = { ...p.autonomy, backoffMs: 60000, nextAt: this.now.getTime() + 60000, lastFingerprint: null };
|
|
340
|
+
}
|
|
259
341
|
const id = randomUUID();
|
|
260
|
-
|
|
342
|
+
// Explicit text-only Think instructions are user input too, even if this
|
|
343
|
+
// episode fails before cognition commits. Empty Think/Dream/scheduler wakes
|
|
344
|
+
// must never replace the latest substantive user instruction.
|
|
345
|
+
const hasMessage = kind === 'send' || (kind === 'think' && (input.trim().length > 0 || attachments.length > 0));
|
|
261
346
|
p.activeEpisodeId = id; p.leaseOwner = workerId; p.leaseUntil = new Date(this.now.getTime() + this.leaseMs);
|
|
262
347
|
p.epoch++; p.writeSerial++; p.inputWatermark++; if (hasMessage) p.messageSeq++;
|
|
263
348
|
this.put('persons', p);
|
|
@@ -269,7 +354,7 @@ export class SqlitePersonStore {
|
|
|
269
354
|
this.put('messages', message, true); this.journal(scope, 'messages', message);
|
|
270
355
|
}
|
|
271
356
|
const episode = this.doc(scope, { id, clientMessageId, requestHash, kind, text: input, messageId, attachments, modelCandidates: [...(p.settings.modelCandidates ?? [])], defaultModel: p.settings.defaultModel ?? null, status: 'running', workerId, epoch: p.epoch,
|
|
272
|
-
baseStateVersion: p.stateVersion, inputWatermark: p.inputWatermark, controlVersion: p.controlVersion, budget, createdAt: this.now });
|
|
357
|
+
baseStateVersion: p.stateVersion, inputWatermark: p.inputWatermark, controlVersion: p.controlVersion, autonomyGeneration: p.settings.autonomyEnabled === true ? p.autonomy?.generation ?? null : null, budget, createdAt: this.now });
|
|
273
358
|
this.put('episodes', episode, true);
|
|
274
359
|
this.trace(ownerId, id, 'accepted', { trigger: { kind, text: input, messageId, attachments }, baseStateVersion: p.stateVersion, budget });
|
|
275
360
|
return { episodeId: id, duplicate: false, status: 'running', episode };
|
|
@@ -326,6 +411,7 @@ export class SqlitePersonStore {
|
|
|
326
411
|
if (!record || record.status !== 'running') fail('STALE');
|
|
327
412
|
const experience = recordCapabilityExperience(p.capabilityExperience, record, kind, data, this.now.toISOString());
|
|
328
413
|
if (experience) p.capabilityExperience = experience;
|
|
414
|
+
if (kind === 'execution_goal') { record.executionGoal = data.goal; this.put('episodes', record); }
|
|
329
415
|
p.writeSerial++; this.put('persons', p);
|
|
330
416
|
return this.trace(episode.ownerId, episode.id, kind, data);
|
|
331
417
|
}
|
|
@@ -352,6 +438,10 @@ export class SqlitePersonStore {
|
|
|
352
438
|
const safeEffective = { model: call.requested.model, effort: typeof effective?.effort === 'string' && effective.effort.length <= 128 ? effective.effort : null,
|
|
353
439
|
effortObserved: effective?.effortObserved === true };
|
|
354
440
|
if (typeof effective?.wireMode === 'string' && effective.wireMode.length <= 128) safeEffective.wireMode = effective.wireMode;
|
|
441
|
+
// Only evidence actually rendered to a successful model call can be
|
|
442
|
+
// acknowledged at commit. Scheduling metadata and inherited refs are not reads.
|
|
443
|
+
if (!failed) record.integratedTaskSourceRefs = [...new Set([...(record.integratedTaskSourceRefs ?? []),
|
|
444
|
+
...(call.manifest?.renderedSourceRefs ?? []).filter(ref => typeof ref === 'string' && ref.startsWith('person-task:'))])].slice(-64);
|
|
355
445
|
delete record.openCall; delete record.callFinalizeUntil; this.put('episodes', record);
|
|
356
446
|
this.trace(episode.ownerId, episode.id, failed ? 'call_failed' : 'call_output', {
|
|
357
447
|
callId, requested: call.requested, effective: safeEffective, output: publicOutput(output, failed),
|
|
@@ -413,6 +503,7 @@ export class SqlitePersonStore {
|
|
|
413
503
|
const data = { callId, ...(started.modelCallId !== undefined ? { modelCallId: started.modelCallId, toolUseId: started.toolUseId, toolIndex: started.toolIndex } : {}), ...(started.capabilityManifest ? { capabilityManifest: started.capabilityManifest } : {}),
|
|
414
504
|
...(failed ? { capabilityId: started.capability.id, code: code || result.code } : { capability: started.capability }),
|
|
415
505
|
...(result !== undefined ? { result } : {}) };
|
|
506
|
+
if (live && started.capability.id === 'Execution.goal' && result?.ok === true) record.executionGoal = result.goal;
|
|
416
507
|
delete record.openCapability; this.put('episodes', record);
|
|
417
508
|
if (live) {
|
|
418
509
|
const experience = recordCapabilityExperience(p.capabilityExperience, record, outcome, data, this.now.toISOString());
|
|
@@ -425,14 +516,18 @@ export class SqlitePersonStore {
|
|
|
425
516
|
}
|
|
426
517
|
/** Exact scoped read in one read transaction; historical evidence grants no lease,
|
|
427
518
|
* publication, capability activation or current behavioral authority. */
|
|
428
|
-
|
|
429
|
-
const request = executionReadRequest(args);
|
|
519
|
+
checkExecutionRead(episode, episodeId = episode.id) {
|
|
430
520
|
this.own(episode);
|
|
431
|
-
const scope = this.episodeScope(episode);
|
|
432
|
-
const live = this.one('episodes', scope, ' AND id = ?', [episode.id]);
|
|
521
|
+
const scope = this.episodeScope(episode), live = this.one('episodes', scope, ' AND id = ?', [episode.id]);
|
|
433
522
|
if (!live || live.status !== 'running' || live.epoch !== episode.epoch || live.workerId !== episode.workerId) fail('STALE');
|
|
434
|
-
const episodeId = request.episodeId ?? episode.id;
|
|
435
523
|
if (!this.one('episodes', scope, ' AND id = ?', [episodeId])) fail('NOT_FOUND');
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
executionRead(episode, args) {
|
|
527
|
+
const request = executionReadRequest(args);
|
|
528
|
+
this.checkExecutionRead(episode, request.episodeId);
|
|
529
|
+
const scope = this.episodeScope(episode), episodeId = request.episodeId ?? episode.id;
|
|
530
|
+
if (request.sourceRef !== undefined) fail('INVALID_REQUEST');
|
|
436
531
|
const trace = this.one('traces', scope,
|
|
437
532
|
" AND json_extract(record, '$.episodeId') = ? AND json_extract(record, '$.callId') = ? AND (json_extract(record, '$.kind') IN ('capability_result', 'capability_failed') OR (json_extract(record, '$.kind') = 'capability_finalized' AND json_extract(record, '$.outcome') IN ('capability_result', 'capability_failed'))) ORDER BY seq DESC", [episodeId, request.callId]);
|
|
438
533
|
if (!trace) fail('NOT_FOUND');
|
|
@@ -469,11 +564,54 @@ export class SqlitePersonStore {
|
|
|
469
564
|
}
|
|
470
565
|
context(episode) {
|
|
471
566
|
const p = this.own(episode), scope = this.episodeScope(episode), state = this.one('states', scope);
|
|
472
|
-
const focused = this.focused(scope, state.focusConceptIds
|
|
473
|
-
const
|
|
567
|
+
const focused = this.focused(scope, state.focusConceptIds.slice(0, 12));
|
|
568
|
+
const latestUser = this.one('messages', scope, " AND json_extract(record, '$.role') = 'user' ORDER BY seq DESC");
|
|
569
|
+
const dream = executionOrientation(episode.kind, episode.scheduling) === 'dream', recentLimit = dream ? 6 : 12;
|
|
570
|
+
const recent = this.rows('concepts', scope, ` ORDER BY updatedAt DESC, id ASC LIMIT ${recentLimit}`);
|
|
571
|
+
let messages = dream ? [] : this.rows('messages', scope, ' ORDER BY seq DESC LIMIT 12');
|
|
572
|
+
let older = [], olderMessages = [];
|
|
573
|
+
if (dream) {
|
|
574
|
+
// Rotate complete records, not LLM summaries or random full-database loads.
|
|
575
|
+
// SQL counts/offsets may scan indexes; only the small selected pages are decoded.
|
|
576
|
+
// inputWatermark advances on admission, including episodes that never commit.
|
|
577
|
+
const rotation = Math.max(0, episode.inputWatermark - 1);
|
|
578
|
+
const recentUsers = this.rows('messages', scope, " AND json_extract(record, '$.role') = 'user' ORDER BY seq DESC LIMIT 6");
|
|
579
|
+
const boundary = recentUsers.at(-1)?.seq ?? 0;
|
|
580
|
+
const userClause = " AND json_extract(record, '$.role') = 'user' AND seq < ?";
|
|
581
|
+
const count = this.sql(`SELECT count(*) AS n FROM messages WHERE ${SCOPE}${userClause}`).get(...scopeValues(scope), boundary).n;
|
|
582
|
+
if (count) {
|
|
583
|
+
const offset = rotation * 4 % count;
|
|
584
|
+
olderMessages = this.rows('messages', scope, `${userClause} ORDER BY seq ASC LIMIT 4 OFFSET ?`, [boundary, offset]);
|
|
585
|
+
if (olderMessages.length < Math.min(4, count)) olderMessages.push(...this.rows('messages', scope,
|
|
586
|
+
`${userClause} ORDER BY seq ASC LIMIT ?`, [boundary, Math.min(4, count) - olderMessages.length]));
|
|
587
|
+
}
|
|
588
|
+
// Assistant progress is not evidence of user interests. Keep at most two
|
|
589
|
+
// substantive replies for conversational grounding, independent of progress volume.
|
|
590
|
+
const replies = this.rows('messages', scope,
|
|
591
|
+
" AND json_extract(record, '$.role') = 'assistant' AND coalesce(json_extract(record, '$.replyKind'), '') != 'progress' ORDER BY seq DESC LIMIT 2");
|
|
592
|
+
messages = [...recentUsers, ...olderMessages, ...replies];
|
|
593
|
+
const excluded = [...new Set([...focused, ...recent].map(c => c.id))];
|
|
594
|
+
const exclusion = excluded.length ? ` AND id NOT IN (${excluded.map(() => '?').join(',')})` : '';
|
|
595
|
+
for (const kind of ['claim', 'question', 'method', 'self-model', 'scenario', 'interest']) {
|
|
596
|
+
const clause = ` AND json_extract(record, '$.kind') = ?${exclusion}`;
|
|
597
|
+
const params = [kind, ...excluded];
|
|
598
|
+
const total = this.sql(`SELECT count(*) AS n FROM concepts WHERE ${SCOPE}${clause}`).get(...scopeValues(scope), ...params).n;
|
|
599
|
+
if (total) older.push(...this.rows('concepts', scope, `${clause} ORDER BY updatedAt ASC, id ASC LIMIT 1 OFFSET ?`, [...params, rotation % total]));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// A long assistant progress stream must never erase the newest user stop or
|
|
603
|
+
// prohibition. Runtime also receives it separately for mandatory admission.
|
|
604
|
+
if (latestUser && !messages.some(m => m.id === latestUser.id)) messages = [...messages.slice(0, 11), latestUser];
|
|
605
|
+
messages.sort((a, b) => a.seq - b.seq);
|
|
474
606
|
return { person: this.personView(p), capabilityExperience: capabilityExperienceView(p.capabilityExperience),
|
|
475
|
-
state: stateView(state),
|
|
476
|
-
concepts: [...new Map([...focused, ...recent].map(c => [c.id, conceptView(c)])).values()]
|
|
607
|
+
state: stateView(state), latestUserInstruction: messageView(latestUser), messages: messages.map(messageView),
|
|
608
|
+
concepts: [...new Map([...focused, ...recent, ...older].map(c => [c.id, conceptView(c)])).values()],
|
|
609
|
+
contextSelection: { strategy: dream ? 'dream-diverse' : 'recent', rotation: dream ? episode.inputWatermark - 1 : null,
|
|
610
|
+
limits: { messages: 12, focusedConcepts: 12, recentConcepts: recentLimit, olderConcepts: dream ? 6 : 0 },
|
|
611
|
+
recentUserMessageRefs: dream ? messages.filter(m => m.role === 'user' && !olderMessages.some(old => old.id === m.id)).map(m => `message:${m.id}:${m.revision}`) : [],
|
|
612
|
+
olderUserMessageRefs: olderMessages.map(m => `message:${m.id}:${m.revision}`),
|
|
613
|
+
focusedConceptRefs: focused.map(c => `concept:${c.id}:${c.revision}`), recentConceptRefs: recent.map(c => `concept:${c.id}:${c.revision}`),
|
|
614
|
+
olderConceptRefs: older.map(c => `concept:${c.id}:${c.revision}`) } };
|
|
477
615
|
}
|
|
478
616
|
/** Atomic capability publication: exact started invocation + live lease, never
|
|
479
617
|
* publishProgress's no-open-capability path. JSON carries the canonical payload;
|
|
@@ -497,7 +635,10 @@ export class SqlitePersonStore {
|
|
|
497
635
|
if (!record || record.status !== 'running' || record.openCall || !proof || record.openCapability?.callId !== callId ||
|
|
498
636
|
record.openCapability.invocationHash !== invocationHash) fail('STALE');
|
|
499
637
|
// Repeating an identical delivery in this episode adds no new visible answer.
|
|
500
|
-
const repeated = prior.find(m => digest(m.presentation) === digest(presentation))
|
|
638
|
+
const repeated = prior.find(m => digest(m.presentation) === digest(presentation)) ??
|
|
639
|
+
(episode.kind === 'autonomous' ? this.rows('messages', scope,
|
|
640
|
+
" AND json_type(record, '$.presentation') = 'object' ORDER BY seq DESC LIMIT 32")
|
|
641
|
+
.find(m => digest(m.presentation) === digest(presentation)) : null);
|
|
501
642
|
if (repeated) {
|
|
502
643
|
this.trace(episode.ownerId, episode.id, 'presentation_published', { callId, invocationHash, messageId: repeated.id, phase: presentation.phase, duplicate: true });
|
|
503
644
|
return messageView(repeated);
|
|
@@ -511,6 +652,11 @@ export class SqlitePersonStore {
|
|
|
511
652
|
this.trace(episode.ownerId, episode.id, 'presentation_published', { callId, invocationHash, messageId: message.id, phase: presentation.phase });
|
|
512
653
|
return messageView(message);
|
|
513
654
|
}
|
|
655
|
+
repeatsAutonomousReply(scope, episode, reply) {
|
|
656
|
+
return episode.kind === 'autonomous' && typeof reply === 'string' &&
|
|
657
|
+
this.rows('messages', scope, " AND json_extract(record, '$.role') = 'assistant' ORDER BY seq DESC LIMIT 32")
|
|
658
|
+
.some(message => message.text.trim() === reply.trim());
|
|
659
|
+
}
|
|
514
660
|
repeatsPresentation(scope, episodeId, reply) {
|
|
515
661
|
return typeof reply === 'string' && this.rows('messages', scope,
|
|
516
662
|
" AND json_extract(record, '$.episodeId') = ? AND json_type(record, '$.presentation') = 'object'", [episodeId])
|
|
@@ -541,9 +687,12 @@ export class SqlitePersonStore {
|
|
|
541
687
|
// cognition does not interpret dedupe as a missing public result.
|
|
542
688
|
const sameReply = prior.find(m => m.text.trim() === reply.trim());
|
|
543
689
|
if (final && sameReply) return messageView(sameReply);
|
|
690
|
+
if (final && this.repeatsAutonomousReply(scope, episode, reply)) {
|
|
691
|
+
return messageView(this.rows('messages', scope, " AND json_extract(record, '$.role') = 'assistant' ORDER BY seq DESC LIMIT 32").find(message => message.text.trim() === reply.trim()));
|
|
692
|
+
}
|
|
544
693
|
// Provider latency counts too: a call started before 30s can finish after it.
|
|
545
694
|
if ((!final && this.now.getTime() - new Date(record.lastReplyAt ?? record.createdAt).getTime() < FEEDBACK.minMs) ||
|
|
546
|
-
sameReply || this.repeatsPresentation(scope, episode.id, reply)) return null;
|
|
695
|
+
sameReply || this.repeatsAutonomousReply(scope, episode, reply) || this.repeatsPresentation(scope, episode.id, reply)) return null;
|
|
547
696
|
p.messageSeq++; p.writeSerial++; this.put('persons', p);
|
|
548
697
|
const message = this.doc(scope, { id: randomUUID(), revision: 1, seq: p.messageSeq, episodeId: episode.id,
|
|
549
698
|
callId, replyKind: final ? 'execution-final' : 'progress', role: 'assistant', text: reply, createdAt: this.now });
|
|
@@ -554,11 +703,12 @@ export class SqlitePersonStore {
|
|
|
554
703
|
}
|
|
555
704
|
commit(episode, proposal, selection, callId, reportedSources = new Map()) {
|
|
556
705
|
const p = this.own(episode), scope = this.episodeScope(episode);
|
|
557
|
-
|
|
706
|
+
const record = this.one('episodes', scope, ' AND id = ?', [episode.id]);
|
|
707
|
+
if (!record || record.status !== 'running' || proposal.baseStateVersion !== episode.baseStateVersion) fail('STALE');
|
|
558
708
|
const repeatsExecutionReply = proposal.reply && this.rows('messages', scope,
|
|
559
709
|
" AND json_extract(record, '$.episodeId') = ? AND json_extract(record, '$.replyKind') IN ('progress', 'execution-final')", [episode.id])
|
|
560
710
|
.some(message => message.text.trim() === proposal.reply.trim());
|
|
561
|
-
const publishReply = proposal.reply && !repeatsExecutionReply && !this.repeatsPresentation(scope, episode.id, proposal.reply);
|
|
711
|
+
const publishReply = proposal.reply && !this.repeatsAutonomousReply(scope, episode, proposal.reply) && !repeatsExecutionReply && !this.repeatsPresentation(scope, episode.id, proposal.reply);
|
|
562
712
|
p.stateVersion++; p.writeSerial++; if (publishReply) p.messageSeq++;
|
|
563
713
|
p.activeEpisodeId = null; p.leaseOwner = null; p.leaseUntil = new Date(0);
|
|
564
714
|
this.put('persons', p);
|
|
@@ -576,18 +726,38 @@ export class SqlitePersonStore {
|
|
|
576
726
|
if (new Set(proposal.state.focusConceptIds).size !== proposal.state.focusConceptIds.length || focus.length !== proposal.state.focusConceptIds.length) fail('STALE');
|
|
577
727
|
const priorState = this.one('states', scope);
|
|
578
728
|
const continuation = Object.hasOwn(proposal.state, 'continuation') ? proposal.state.continuation : priorState?.continuation;
|
|
579
|
-
const
|
|
729
|
+
const understanding = Object.hasOwn(proposal.state, 'understanding') ? proposal.state.understanding : priorState?.understanding;
|
|
730
|
+
const state = this.doc(scope, { ...proposal.state, ...(continuation !== undefined ? { continuation } : {}),
|
|
731
|
+
...(understanding !== undefined ? { understanding } : {}), version: p.stateVersion, conceptRefs: focus.map(c => ({ id: c.id, revision: c.revision })),
|
|
580
732
|
decision: proposal.decision, lastSelection: { model: selection.model, effort: selection.effort }, updatedAt: this.now, episodeId: episode.id });
|
|
581
733
|
if (this.one('states', scope)?.version !== episode.baseStateVersion) fail('STALE');
|
|
582
734
|
this.put('states', state);
|
|
735
|
+
if (episode.kind === 'autonomous' && p.autonomy?.generation === episode.autonomyGeneration) {
|
|
736
|
+
p.autonomy.lastFingerprint = digest(continuation ?? null);
|
|
737
|
+
// Backoff starts when discovery actually rests, not when a potentially long
|
|
738
|
+
// episode was admitted. Never immediately wake after a long execution.
|
|
739
|
+
if (record.executionGoal?.kind === 'exploration' && record.executionGoal.status === 'resting' && continuation == null) {
|
|
740
|
+
p.autonomy.nextAt = this.now.getTime() + (p.autonomy.backoffMs ?? 60000);
|
|
741
|
+
}
|
|
742
|
+
this.put('persons', p);
|
|
743
|
+
}
|
|
583
744
|
this.put('state_commits', this.doc(scope, { id: randomUUID(), version: p.stateVersion, parentVersion: episode.baseStateVersion,
|
|
584
745
|
episodeId: episode.id, callId, conceptRevisions: revisions, state: stateView(state), createdAt: this.now }), true);
|
|
585
746
|
if (publishReply) {
|
|
586
747
|
const message = this.doc(scope, { id: randomUUID(), revision: 1, seq: p.messageSeq, episodeId: episode.id, callId, replyKind: 'final', role: 'assistant', text: proposal.reply, createdAt: this.now });
|
|
587
748
|
this.put('messages', message, true); this.journal(scope, 'messages', message);
|
|
588
749
|
}
|
|
589
|
-
|
|
590
|
-
|
|
750
|
+
if (p.settings.autonomyEnabled === true && p.autonomy?.generation === record.autonomyGeneration) {
|
|
751
|
+
const integrated = new Set(record.integratedTaskSourceRefs ?? []);
|
|
752
|
+
if (integrated.size) {
|
|
753
|
+
// Exact event identity only. A completion arriving after the last model
|
|
754
|
+
// read survives; rollback of any rejected commit preserves every event.
|
|
755
|
+
p.autonomy.pending = (p.autonomy.pending ?? []).filter(event => !integrated.has(event.sourceRef));
|
|
756
|
+
p.autonomy.integratedSourceRefs = [...new Set([...(p.autonomy.integratedSourceRefs ?? []), ...integrated])].slice(-64);
|
|
757
|
+
p.autonomy.lastFingerprint = digest(continuation ?? null);
|
|
758
|
+
this.put('persons', p);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
591
761
|
delete record.executionControl;
|
|
592
762
|
Object.assign(record, { status: 'completed', endedAt: this.now, stateVersion: p.stateVersion }); this.put('episodes', record);
|
|
593
763
|
this.trace(episode.ownerId, episode.id, 'committed', { callId, parentVersion: episode.baseStateVersion, stateVersion: p.stateVersion, conceptRevisions: revisions, decision: proposal.decision });
|
|
@@ -607,7 +777,17 @@ export class SqlitePersonStore {
|
|
|
607
777
|
}
|
|
608
778
|
cancel(ownerId, episodeId = null) {
|
|
609
779
|
const scope = this.scope(ownerId), p = this.getPerson(ownerId);
|
|
610
|
-
|
|
780
|
+
// Cancel is also a durable autonomy stop, even when the target already ended.
|
|
781
|
+
p.settings = { ...p.settings, autonomyEnabled: false };
|
|
782
|
+
p.autonomy = { version: 1, generation: randomUUID(), pending: [], nextAt: null };
|
|
783
|
+
p.writeSerial++; this.put('persons', p);
|
|
784
|
+
if (!p.activeEpisodeId) return { cancelled: false, episodeId };
|
|
785
|
+
if (episodeId && p.activeEpisodeId !== episodeId) {
|
|
786
|
+
const active = this.one('episodes', scope, ' AND id = ?', [p.activeEpisodeId]);
|
|
787
|
+
if (active?.kind !== 'autonomous') return { cancelled: false, episodeId };
|
|
788
|
+
// The target may finish just before a queued autonomous admission. Cancel
|
|
789
|
+
// that follow-up too; never let a stale target bypass the owner's stop.
|
|
790
|
+
}
|
|
611
791
|
const id = p.activeEpisodeId, until = p.leaseUntil;
|
|
612
792
|
p.epoch++; p.controlVersion++; p.writeSerial++; p.activeEpisodeId = null; p.leaseOwner = null; p.leaseUntil = new Date(0); this.put('persons', p);
|
|
613
793
|
const record = this.one('episodes', scope, ' AND id = ?', [id]);
|
|
@@ -618,14 +798,21 @@ export class SqlitePersonStore {
|
|
|
618
798
|
this.trace(ownerId, id, 'cancelled', { code: 'CANCELLED' });
|
|
619
799
|
return { cancelled: true, episodeId: id };
|
|
620
800
|
}
|
|
621
|
-
settings(ownerId, settings, expectedControlVersion) {
|
|
801
|
+
settings(ownerId, settings, expectedControlVersion, expectedAutonomyGeneration) {
|
|
622
802
|
const p = this.getPerson(ownerId);
|
|
623
|
-
|
|
803
|
+
const autonomyOnly = Object.keys(settings).length === 1 && Object.hasOwn(settings, 'autonomyEnabled');
|
|
804
|
+
if (p.activeEpisodeId && !autonomyOnly) fail('BUSY');
|
|
805
|
+
if (expectedAutonomyGeneration !== undefined && (p.autonomy?.generation ?? null) !== expectedAutonomyGeneration) fail('STALE');
|
|
806
|
+
if (Object.hasOwn(settings, 'autonomyEnabled')) {
|
|
807
|
+
if (typeof settings.autonomyEnabled !== 'boolean') fail('INVALID_REQUEST');
|
|
808
|
+
p.autonomy = { version: 1, generation: randomUUID(), pending: [], backoffMs: 60000,
|
|
809
|
+
nextAt: settings.autonomyEnabled ? this.now.getTime() + 5000 : null };
|
|
810
|
+
}
|
|
624
811
|
if (expectedControlVersion !== undefined && p.controlVersion !== expectedControlVersion) fail('STALE');
|
|
625
812
|
const { name, ...patch } = settings;
|
|
626
813
|
if (Object.hasOwn(settings, 'name')) p.name = personName(name);
|
|
627
814
|
// A partial update must preserve durable fields omitted from the public view.
|
|
628
|
-
p.settings = { ...p.settings, ...patch }; p.controlVersion++; p.writeSerial++; this.put('persons', p);
|
|
815
|
+
p.settings = { ...p.settings, ...patch }; if (!autonomyOnly) p.controlVersion++; p.writeSerial++; this.put('persons', p);
|
|
629
816
|
if (Object.keys(settings).length) this.trace(ownerId, null, 'settings', { settings: settingsView(p.settings), ...(name !== undefined ? { name: p.name } : {}) });
|
|
630
817
|
return { settings: settingsView(p.settings), person: this.personView(p) };
|
|
631
818
|
}
|
|
@@ -4,7 +4,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { TaskManager } from '../tasks/manager.js';
|
|
5
5
|
import { TASK_RESULT_DELIVERY, isTerminalTaskStatus } from '../tasks/store.js';
|
|
6
6
|
import { createFullRegistry } from '../tools/index.js';
|
|
7
|
-
import { ToolRegistry, normalizeToolOutput } from '../tools/registry.js';
|
|
7
|
+
import { ToolRegistry, normalizeToolOutput, isToolErrorOutput, toolErrorEffect } from '../tools/registry.js';
|
|
8
8
|
import { SubAgentToolRegistry } from '../sub-agent/execution-control.js';
|
|
9
9
|
import { agentBelongsToScope, getAgentRegistry } from '../tools/agent.js';
|
|
10
10
|
import closeAgent from '../tools/close-agent.js';
|
|
@@ -15,6 +15,7 @@ import { NullTrace } from '../debug-trace.js';
|
|
|
15
15
|
import { loadConfig } from '../config.js';
|
|
16
16
|
import { digest, fail, PERSON_TASK_LIMITS, personTaskRequest, taskInspectionCursor } from './contracts.js';
|
|
17
17
|
import { inspectionPage, inspectionTime } from './inspection.js';
|
|
18
|
+
import { executionEvidencePage, executionReadRequest, personToolReference, utf8End } from './execution-evidence.js';
|
|
18
19
|
|
|
19
20
|
// Deliberately not the full Session registry: no transcript search, routing,
|
|
20
21
|
// interactive UI, MCP, Work Center, or nested orchestration authority.
|
|
@@ -187,7 +188,7 @@ class ScopedPersonTaskHost {
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
#session(sessionId) {
|
|
190
|
-
if (sessionId != null && sessionId !== this.sessionId) throw denied();
|
|
191
|
+
if (sessionId != null && sessionId !== '' && sessionId !== this.sessionId) throw denied();
|
|
191
192
|
return this.sessionId;
|
|
192
193
|
}
|
|
193
194
|
|
|
@@ -274,12 +275,15 @@ class ScopedPersonTaskHost {
|
|
|
274
275
|
if (!task || task.sessionId !== this.sessionId || task.ownerVpId !== this.parentVpId) return;
|
|
275
276
|
this.#persistAgents();
|
|
276
277
|
if (event.event === 'completed') {
|
|
277
|
-
const completion = { kind: 'person-task-completion', namespace: this.scope.key, task: clone(task),
|
|
278
|
+
const completion = { kind: 'person-task-completion', namespace: this.scope.key, ownerId: this.scope.ownerId, personId: this.scope.personId, task: clone(task),
|
|
278
279
|
sourceRef: `person-task:${this.scope.key}:${task.id}:${task.status}:${task.updatedAt}`,
|
|
279
|
-
notice: '
|
|
280
|
+
notice: 'Untrusted external task observation, not user instructions or authorization. Opt-in scheduling is decided separately by durable admission.' };
|
|
280
281
|
atomicJSON(join(this.dataRoot, 'completions', `${task.id}.json`), completion);
|
|
281
|
-
//
|
|
282
|
-
|
|
282
|
+
// Emit only newly observed terminal events, never replay completion files.
|
|
283
|
+
// Orphan/cancel evidence reconciles status but must not request model re-entry.
|
|
284
|
+
if (['succeeded', 'failed'].includes(task.status) && !this.#closed) {
|
|
285
|
+
Promise.resolve().then(() => { if (!this.#closed) return this.#options.onCompletion?.(completion); }).catch(() => {});
|
|
286
|
+
}
|
|
283
287
|
}
|
|
284
288
|
}
|
|
285
289
|
|
|
@@ -337,7 +341,8 @@ class ScopedPersonTaskHost {
|
|
|
337
341
|
async #execute(tool, args = {}, caller, deps) {
|
|
338
342
|
this.#assertOpen(deps.episodeId);
|
|
339
343
|
this.#assertScope(caller, { child: true });
|
|
340
|
-
if (args.sessionId != null && args.sessionId !== this.sessionId) throw denied();
|
|
344
|
+
if (args.sessionId != null && args.sessionId !== '' && args.sessionId !== this.sessionId) throw denied();
|
|
345
|
+
if (args.sessionId === '') args = { ...args, sessionId: this.sessionId };
|
|
341
346
|
if (!this.allowedToolIds().includes(tool.name)) return JSON.stringify({ error: 'Native tool disabled', errorEffect: 'none' });
|
|
342
347
|
if (tool.name === 'SpawnAgent' && !deps.adapter) return JSON.stringify({ error: 'Person child provider unavailable', errorEffect: 'none' });
|
|
343
348
|
const controller = new AbortController();
|
|
@@ -402,11 +407,45 @@ class ScopedPersonTaskHost {
|
|
|
402
407
|
#archiveToolResult(job, output, error) {
|
|
403
408
|
const text = error ? String(error.message || error) : normalizeToolOutput(output);
|
|
404
409
|
const result = { id: job.id, tool: job.tool, episodeId: job.episodeId, childId: job.childId,
|
|
405
|
-
updatedAt: new Date().toISOString(), ok: !error, aborted: job.controller.signal.aborted,
|
|
410
|
+
updatedAt: new Date().toISOString(), ok: !error && !isToolErrorOutput(text), aborted: job.controller.signal.aborted,
|
|
411
|
+
...(error ? { code: 'TOOL_FAILED', errorEffect: 'unknown' } : isToolErrorOutput(text) ? { code: 'TOOL_FAILED', errorEffect: toolErrorEffect(text) } : {}),
|
|
406
412
|
output: text, sourceRef: `person-tool:${this.scope.key}:${job.id}` };
|
|
407
413
|
atomicJSON(join(this.dataRoot, 'tool-results', `${job.id}.json`), result);
|
|
408
414
|
}
|
|
409
415
|
|
|
416
|
+
/** Exact owned archive only. Repository authorization is required before IO
|
|
417
|
+
* and again before returning bytes; neither model paths nor scopes authorize it. */
|
|
418
|
+
async readToolResult(episode, args, { repository, signal } = {}) {
|
|
419
|
+
signal?.throwIfAborted();
|
|
420
|
+
this.#assertOpen(episode?.id);
|
|
421
|
+
if (!episode?.id || episode.ownerId !== this.scope.ownerId || episode.personId !== this.scope.personId ||
|
|
422
|
+
episode.namespace !== this.scope.namespace) throw denied();
|
|
423
|
+
const request = executionReadRequest(args);
|
|
424
|
+
const { scope, id } = personToolReference(request.sourceRef);
|
|
425
|
+
if (scope !== this.scope.key) throw denied();
|
|
426
|
+
if (!repository?.checkExecutionRead) fail('UNSUPPORTED');
|
|
427
|
+
await repository.checkExecutionRead(episode, request.episodeId);
|
|
428
|
+
signal?.throwIfAborted();
|
|
429
|
+
const path = privatePath(this.yeaftDir, join(this.dataRoot, 'tool-results', `${id}.json`));
|
|
430
|
+
let result;
|
|
431
|
+
try {
|
|
432
|
+
if (!lstatSync(path).isFile()) throw denied();
|
|
433
|
+
result = JSON.parse(readFileSync(path, 'utf8'));
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (error.code === 'ENOENT') fail('NOT_FOUND');
|
|
436
|
+
if (error.code === 'TASK_SCOPE_DENIED') throw error;
|
|
437
|
+
fail('STORAGE_UNAVAILABLE');
|
|
438
|
+
}
|
|
439
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) fail('STORAGE_UNAVAILABLE');
|
|
440
|
+
if (result.id !== id || result.sourceRef !== request.sourceRef || result.episodeId !== (request.episodeId ?? episode.id)) throw denied();
|
|
441
|
+
if (typeof result.output !== 'string' || typeof result.tool !== 'string' || typeof result.ok !== 'boolean') fail('STORAGE_UNAVAILABLE');
|
|
442
|
+
const page = executionEvidencePage({ episodeId: result.episodeId, result, capabilityId: result.tool }, null, request, episode.id);
|
|
443
|
+
await repository.checkExecutionRead(episode, request.episodeId);
|
|
444
|
+
signal?.throwIfAborted();
|
|
445
|
+
this.#assertOpen(episode.id);
|
|
446
|
+
return page;
|
|
447
|
+
}
|
|
448
|
+
|
|
410
449
|
#instrumentChild(agent, deps) {
|
|
411
450
|
if (!agent || !agentBelongsToScope(agent, this.agentScope)) return;
|
|
412
451
|
agent.personEpisodeId = deps.episodeId;
|
|
@@ -571,6 +610,17 @@ class ScopedPersonTaskHost {
|
|
|
571
610
|
}
|
|
572
611
|
}
|
|
573
612
|
|
|
613
|
+
/** Exact owner-scoped control state, never the capped model evidence page. */
|
|
614
|
+
goalState(episodeId) {
|
|
615
|
+
this.#persistAgents();
|
|
616
|
+
return {
|
|
617
|
+
tasks: this.#manager.listActiveTasks(this.sessionId, this.parentVpId).map(task => ({ id: task.id, status: task.status,
|
|
618
|
+
currentEpisode: task.source?.episodeId === episodeId })),
|
|
619
|
+
agents: [...this.#agents.values()].map(agent => ({ id: agent.id, status: agent.status,
|
|
620
|
+
executionPending: agent.executionPending, currentEpisode: agent.personEpisodeId === episodeId })),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
574
624
|
/** Completion records remain facts, not user messages or automatic triggers. */
|
|
575
625
|
evidence({ limit = 50 } = {}) {
|
|
576
626
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100) throw new Error('Invalid evidence limit');
|
|
@@ -579,8 +629,11 @@ class ScopedPersonTaskHost {
|
|
|
579
629
|
return { namespace: this.scope.key, sessionId: this.sessionId,
|
|
580
630
|
tasks: recent(jsonFiles(this.#manager.store.sessionDir(this.sessionId), { limit })),
|
|
581
631
|
agents: clone([...this.#agents.values()].slice(-limit)), completions: recent(jsonFiles(join(this.dataRoot, 'completions'), { limit })),
|
|
582
|
-
toolResults: recent(jsonFiles(join(this.dataRoot, 'tool-results'), { limit })).map(result =>
|
|
583
|
-
|
|
632
|
+
toolResults: recent(jsonFiles(join(this.dataRoot, 'tool-results'), { limit })).map(result => {
|
|
633
|
+
const raw = Buffer.from(result.output, 'utf8'), end = utf8End(raw, Math.min(raw.length, 8192));
|
|
634
|
+
return { ...result, rawBytes: raw.length, sha256: digest(result.output), output: raw.subarray(0, end).toString('utf8'),
|
|
635
|
+
...(end < raw.length ? { outputTruncated: true, outputNextOffset: end } : {}), rawPath: join(this.dataRoot, 'tool-results', `${result.id}.json`) };
|
|
636
|
+
}) };
|
|
584
637
|
}
|
|
585
638
|
|
|
586
639
|
/** Explicit cancellation stops and joins this Person's effects; normal episode
|
|
@@ -672,8 +725,25 @@ export class PersonTaskHost {
|
|
|
672
725
|
return this.#host(episode.ownerId, episode.personId).context(input);
|
|
673
726
|
}
|
|
674
727
|
|
|
728
|
+
goalState(ownerId, episodeId) {
|
|
729
|
+
return this.#host(ownerId).goalState(episodeId);
|
|
730
|
+
}
|
|
731
|
+
|
|
675
732
|
snapshot(ownerId) { return this.#host(ownerId).snapshot(ownerId); }
|
|
676
733
|
|
|
734
|
+
async readToolResult(episode, args, options) {
|
|
735
|
+
if (!episode?.id || episode.namespace !== this.#options.namespace) throw denied();
|
|
736
|
+
return this.#host(episode.ownerId, episode.personId).readToolResult(episode, args, options);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
toolResultRef(episode, callId) {
|
|
740
|
+
if (!episode?.id || episode.namespace !== this.#options.namespace) throw denied();
|
|
741
|
+
const host = this.#host(episode.ownerId, episode.personId);
|
|
742
|
+
const ref = `person-tool:${host.scope.key}:${callId}`;
|
|
743
|
+
personToolReference(ref);
|
|
744
|
+
return ref;
|
|
745
|
+
}
|
|
746
|
+
|
|
677
747
|
request(input = {}) {
|
|
678
748
|
if (typeof input.ownerId !== 'string' || !input.ownerId || typeof input.personId !== 'string' || !input.personId
|
|
679
749
|
|| input.namespace !== this.#options.namespace) throw denied();
|
|
@@ -4,6 +4,7 @@ import { executionReadRequest } from './execution-evidence.js';
|
|
|
4
4
|
import { outputArgs } from './outputs.js';
|
|
5
5
|
import { personPresentation } from './presentation.js';
|
|
6
6
|
import { scriptInput } from './script-executor.js';
|
|
7
|
+
import { validateExecutionGoal } from './execution-goal.js';
|
|
7
8
|
|
|
8
9
|
/** Pre-dispatch schema check, deliberately separate from execution: rejection
|
|
9
10
|
* here proves that no host effect has begun and can safely become tool feedback.
|
|
@@ -50,7 +51,8 @@ export function validateToolArguments(value, schema) {
|
|
|
50
51
|
* callers must not wrap capability execution in the same catch. */
|
|
51
52
|
export function validateCapabilityArguments({ id, args }) {
|
|
52
53
|
try {
|
|
53
|
-
if (id === '
|
|
54
|
+
if (id === 'Execution.goal') validateExecutionGoal(args);
|
|
55
|
+
else if (id === 'Capability.create') validateCreatedDefinition(args);
|
|
54
56
|
else if (id === 'Execution.read') executionReadRequest(args);
|
|
55
57
|
else if (id === 'Output.publish') outputArgs(args);
|
|
56
58
|
else if (id === 'PresentResponse') personPresentation(args);
|