@yeaft/webchat-agent 1.0.595 → 1.0.597
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 +2 -1
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +223 -154
- 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 +19 -0
- package/yeaft/person/inspection.js +60 -0
- package/yeaft/person/repository.js +37 -12
- package/yeaft/person/service.js +11 -2
- package/yeaft/person/sqlite-repository.js +2 -0
- package/yeaft/person/sqlite-store.js +35 -15
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { bytes, digest, fail, identifier, object, page, text } from './contracts.js';
|
|
2
2
|
import { createdCapabilityRecord, validateCreatedDefinition } from './created-capability-contract.js';
|
|
3
3
|
import { runPersonScript, scriptInput, testPersonScript } from './script-executor.js';
|
|
4
|
+
import { createdSkillView, inspectionPage } from './inspection.js';
|
|
4
5
|
|
|
5
6
|
// Built-in cognition plus bounded pure-script creation, not the Agent's full registry.
|
|
6
7
|
// No shell/filesystem/network/VP authority is exposed to generated code.
|
|
@@ -65,6 +66,24 @@ function scriptManifest(record) {
|
|
|
65
66
|
origin: 'person-created', evidence: record.evidence };
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/** Read the real catalog without preparing or executing any capability. No host
|
|
70
|
+
* registry, file lookup, model configuration or script VM participates. */
|
|
71
|
+
export function inspectCapabilities(created, { cursor, limit }) {
|
|
72
|
+
const entries = manifests.map(contract => ({ id: contract.id, domain: contract.domain, description: contract.description,
|
|
73
|
+
version: contract.version, revision: contract.revision, contract: copy(contract), source: { kind: 'builtin' } }));
|
|
74
|
+
for (const record of created) {
|
|
75
|
+
const contract = scriptManifest(record);
|
|
76
|
+
// Evidence is a fixed public shape even when reading older raw records.
|
|
77
|
+
const item = createdSkillView(record, contract);
|
|
78
|
+
contract.evidence = item.evidence;
|
|
79
|
+
entries.push(item);
|
|
80
|
+
}
|
|
81
|
+
// IDs are ASCII identifiers. Binary ordering matches SQLite and Mongo simple collation.
|
|
82
|
+
const matches = entries.filter(entry => cursor === null || entry.id > cursor)
|
|
83
|
+
.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
84
|
+
return inspectionPage(matches, limit, 'id');
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
export class PersonCapabilities {
|
|
69
88
|
constructor(repository, ownerId, { experience = [], triggerKind = 'think', now = Date.now(), created = [], episode } = {}) {
|
|
70
89
|
this.repository = repository; this.ownerId = ownerId; this.episode = episode;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { bytes, fail, identifier, object, page, text } from './contracts.js';
|
|
2
|
+
|
|
3
|
+
/** Public read APIs use keyset cursors, never offsets or model-ranked recall. */
|
|
4
|
+
export function inspectRequest(payload = {}) {
|
|
5
|
+
object(payload, ['section', 'cursor', 'limit'], ['section']);
|
|
6
|
+
if (!['memory', 'skills'].includes(payload.section)) fail('INVALID_REQUEST');
|
|
7
|
+
const { limit } = page({ limit: payload.limit });
|
|
8
|
+
const cursor = payload.cursor ?? null;
|
|
9
|
+
if (cursor !== null) identifier(cursor);
|
|
10
|
+
return { section: payload.section, cursor, limit };
|
|
11
|
+
}
|
|
12
|
+
export function searchRequest(payload = {}) {
|
|
13
|
+
object(payload, ['query', 'cursor', 'limit'], ['query']);
|
|
14
|
+
return { query: text(payload.query, 512), ...page({ cursor: payload.cursor, limit: payload.limit }) };
|
|
15
|
+
}
|
|
16
|
+
export function personName(value) {
|
|
17
|
+
if (typeof value !== 'string') fail('INVALID_REQUEST');
|
|
18
|
+
return text(value.trim(), 160);
|
|
19
|
+
}
|
|
20
|
+
const pick = (value, keys) => value == null ? null : Object.fromEntries(keys.filter(key => Object.hasOwn(value, key)).map(key => [key, value[key]]));
|
|
21
|
+
export const settingsView = value => pick(value, ['autonomyEnabled', 'modelCandidates']);
|
|
22
|
+
export function messageView(value) {
|
|
23
|
+
const result = pick(value, ['id', 'schemaVersion', 'revision', 'seq', 'episodeId', 'clientMessageId', 'role', 'text', 'createdAt']);
|
|
24
|
+
if (result && Array.isArray(value.attachments)) result.attachments = value.attachments.slice(0, 4)
|
|
25
|
+
.map(file => pick(file, ['id', 'name', 'mimeType', 'size', 'sha256', 'kind']));
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
export function conceptView(value) {
|
|
29
|
+
const result = pick(value, ['id', 'schemaVersion', 'kind', 'statement', 'epistemicState', 'sourceRefs', 'reportedSourceRefs', 'revision', 'episodeId', 'callId', 'stateVersion', 'updatedAt']);
|
|
30
|
+
if (result && Array.isArray(value.associations)) result.associations = value.associations.map(a => pick(a, ['targetId', 'relation']));
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
export function stateView(value) {
|
|
34
|
+
const result = pick(value, ['schemaVersion', 'version', 'summary', 'appraisal', 'focusConceptIds', 'updatedAt', 'episodeId']);
|
|
35
|
+
if (!result) return null;
|
|
36
|
+
if (Array.isArray(value.conceptRefs)) result.conceptRefs = value.conceptRefs.map(ref => pick(ref, ['id', 'revision']));
|
|
37
|
+
if (Object.hasOwn(value, 'decision')) result.decision = pick(value.decision, ['summary', 'uncertainties', 'selfCheck']);
|
|
38
|
+
if (Object.hasOwn(value, 'lastSelection')) result.lastSelection = pick(value.lastSelection, ['model', 'effort']);
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
export function createdSkillView(record, contract) {
|
|
42
|
+
return { ...pick(record, ['id', 'description', 'version', 'revision', 'code', 'inputDescription', 'outputDescription', 'createdAt', 'updatedAt']),
|
|
43
|
+
tests: Array.isArray(record.tests) ? record.tests.map(test => pick(test, ['input', 'expected'])) : [],
|
|
44
|
+
domain: 'script', contract,
|
|
45
|
+
evidence: pick(record.evidence, ['engine', 'testsPassed', 'testedAt']),
|
|
46
|
+
source: { kind: 'person-created', episodeId: record.createdEpisodeId, callId: record.createdCallId } };
|
|
47
|
+
}
|
|
48
|
+
/** Complete records only. Continuation follows the last returned key even when the
|
|
49
|
+
* byte budget, rather than the requested count, ends a page. No attachment bytes. */
|
|
50
|
+
export function inspectionPage(records, limit, key, view = value => value) {
|
|
51
|
+
const items = []; let size = 0;
|
|
52
|
+
for (const record of records) {
|
|
53
|
+
if (items.length === limit) break;
|
|
54
|
+
const item = view(record), cost = bytes(item);
|
|
55
|
+
if (cost > 256 * 1024) fail('OUTPUT_LIMIT');
|
|
56
|
+
if (items.length && size + cost > 256 * 1024) break;
|
|
57
|
+
items.push(item); size += cost;
|
|
58
|
+
}
|
|
59
|
+
return { items, nextCursor: records.length > items.length ? String(items.at(-1)[key]) : null };
|
|
60
|
+
}
|
|
@@ -3,6 +3,8 @@ import { admissionReceipt, bytes, digest, fail, identifier, LIMITS, PersonError,
|
|
|
3
3
|
import { CREATED_CAPABILITY_LIMITS, createdCapabilityRecord, validateCreatedCapability } from './created-capability-contract.js';
|
|
4
4
|
import { attachmentMetadata, attachmentRequestHash, validateFiles } from './attachments.js';
|
|
5
5
|
import { capabilityExperienceView, recordCapabilityExperience } from './capability-experience.js';
|
|
6
|
+
import { conceptView, inspectionPage, inspectRequest, messageView, personName, searchRequest, settingsView, stateView } from './inspection.js';
|
|
7
|
+
import { inspectCapabilities } from './capabilities.js';
|
|
6
8
|
|
|
7
9
|
const COLLECTIONS = ['persons', 'attachments', 'messages', 'episodes', 'states', 'concepts', 'concept_revisions', 'state_commits', 'traces', 'created_capabilities', 'created_capability_revisions'];
|
|
8
10
|
const txOptions = { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }, readPreference: 'primary', maxCommitTimeMS: 5000, timeoutMS: 10000 };
|
|
@@ -97,7 +99,7 @@ export class MongoPersonRepository {
|
|
|
97
99
|
finally { await session.endSession(); }
|
|
98
100
|
}
|
|
99
101
|
personView(p) {
|
|
100
|
-
return { id: p.personId, name: p.name, soul: p.soul, soulRevision: p.soulRevision, createdAt: iso(p.createdAt), settings: p.settings };
|
|
102
|
+
return { id: p.personId, name: p.name, soul: p.soul, soulRevision: p.soulRevision, createdAt: iso(p.createdAt), settings: settingsView(p.settings) };
|
|
101
103
|
}
|
|
102
104
|
async open(ownerId, name = 'Digital Person') {
|
|
103
105
|
const scope = this.scope(ownerId);
|
|
@@ -334,9 +336,9 @@ export class MongoPersonRepository {
|
|
|
334
336
|
this.collections.states.findOne(scope).then(s => this.collections.concepts.find({ ...scope, id: { $in: s.focusConceptIds } }).limit(12).toArray()),
|
|
335
337
|
this.collections.concepts.find(scope).sort({ updatedAt: -1, id: 1 }).limit(12).toArray(),
|
|
336
338
|
]);
|
|
337
|
-
const concepts = [...new Map([...focused, ...recent].map(c => [c.id,
|
|
339
|
+
const concepts = [...new Map([...focused, ...recent].map(c => [c.id, conceptView(c)])).values()];
|
|
338
340
|
return { person: this.personView(p), capabilityExperience: capabilityExperienceView(p.capabilityExperience),
|
|
339
|
-
state:
|
|
341
|
+
state: stateView(state), messages: messages.reverse().map(messageView), concepts };
|
|
340
342
|
}
|
|
341
343
|
async recall(ownerId, { kind = 'messages', query = '', cursor = null, limit = 5 }) {
|
|
342
344
|
const scope = this.scope(ownerId);
|
|
@@ -345,7 +347,7 @@ export class MongoPersonRepository {
|
|
|
345
347
|
await this.init();
|
|
346
348
|
const filter = { ...scope, ...(query ? { statement: { $regex: escaped, $options: 'i' } } : {}), ...(cursor ? { id: { $gt: cursor } } : {}) };
|
|
347
349
|
const docs = await this.collections.concepts.find(filter).sort({ id: 1 }).limit(limit + 1).maxTimeMS(2000).toArray();
|
|
348
|
-
return { items: docs.slice(0, limit).map(
|
|
350
|
+
return { items: docs.slice(0, limit).map(conceptView), nextCursor: docs.length > limit ? docs[limit - 1].id : null };
|
|
349
351
|
}
|
|
350
352
|
async commit(episode, proposal, selection, callId, reportedSources = new Map()) {
|
|
351
353
|
return this.transaction(async session => {
|
|
@@ -376,12 +378,12 @@ export class MongoPersonRepository {
|
|
|
376
378
|
const updated = await this.collections.states.replaceOne({ ...scope, version: episode.baseStateVersion }, state, { session });
|
|
377
379
|
if (updated.matchedCount !== 1) fail('STALE');
|
|
378
380
|
await this.collections.state_commits.insertOne(this.doc(scope, { id: randomUUID(), version: p.stateVersion, parentVersion: episode.baseStateVersion,
|
|
379
|
-
episodeId: episode.id, callId, conceptRevisions: revisions, state:
|
|
381
|
+
episodeId: episode.id, callId, conceptRevisions: revisions, state: stateView(state), createdAt: new Date() }), { session });
|
|
380
382
|
if (proposal.reply) await this.collections.messages.insertOne(this.doc(scope, { id: randomUUID(), revision: 1, seq: p.messageSeq, episodeId: episode.id,
|
|
381
383
|
role: 'assistant', text: proposal.reply, createdAt: new Date() }), { session });
|
|
382
384
|
await this.collections.episodes.updateOne({ ...scope, id: episode.id, status: 'running' }, { $set: { status: 'completed', endedAt: new Date(), stateVersion: p.stateVersion } }, { session });
|
|
383
385
|
await this.trace(session, p, episode.id, 'committed', { callId, parentVersion: episode.baseStateVersion, stateVersion: p.stateVersion, conceptRevisions: revisions, decision: proposal.decision });
|
|
384
|
-
return { state:
|
|
386
|
+
return { state: stateView(state) };
|
|
385
387
|
});
|
|
386
388
|
}
|
|
387
389
|
async finish(episode, status, code) {
|
|
@@ -416,16 +418,39 @@ export class MongoPersonRepository {
|
|
|
416
418
|
const p = await this.collections.persons.findOne(scope, { session });
|
|
417
419
|
if (!p) fail('NOT_OPEN');
|
|
418
420
|
if (p.activeEpisodeId) fail('BUSY'); // No implicit background API or deferred control effects.
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
421
|
+
const { name, ...patch } = settings;
|
|
422
|
+
if (Object.hasOwn(settings, 'name')) p.name = personName(name);
|
|
423
|
+
const next = { ...settingsView(p.settings), ...patch };
|
|
424
|
+
await this.collections.persons.updateOne(scope, { $set: { settings: next, name: p.name }, $inc: { controlVersion: 1, writeSerial: 1 } }, { session });
|
|
425
|
+
if (Object.keys(settings).length) await this.trace(session, p, null, 'settings', { settings: next, ...(name !== undefined ? { name: p.name } : {}) });
|
|
426
|
+
return { settings: next, person: this.personView({ ...p, settings: next }) };
|
|
423
427
|
});
|
|
424
428
|
}
|
|
429
|
+
async inspect(ownerId, options) {
|
|
430
|
+
const { section, cursor, limit } = inspectRequest(options);
|
|
431
|
+
await this.getPerson(ownerId);
|
|
432
|
+
const scope = this.scope(ownerId);
|
|
433
|
+
if (section === 'skills') {
|
|
434
|
+
const docs = await this.collections.created_capabilities.find(scope).sort({ id: 1 }).limit(32).maxTimeMS(2000).toArray();
|
|
435
|
+
return inspectCapabilities(docs.map(doc => JSON.parse(doc.record)), { cursor, limit });
|
|
436
|
+
}
|
|
437
|
+
const records = await this.collections.concepts.find({ ...scope, ...(cursor !== null ? { id: { $gt: cursor } } : {}) })
|
|
438
|
+
.collation({ locale: 'simple' }).sort({ id: 1 }).limit(limit + 1).maxTimeMS(2000).toArray();
|
|
439
|
+
return inspectionPage(records, limit, 'id', conceptView);
|
|
440
|
+
}
|
|
441
|
+
async search(ownerId, options) {
|
|
442
|
+
const { query, cursor, limit } = searchRequest(options);
|
|
443
|
+
await this.getPerson(ownerId);
|
|
444
|
+
const records = await this.collections.messages.find({ ...this.scope(ownerId),
|
|
445
|
+
text: { $regex: query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' }, ...(cursor !== null ? { seq: { $lt: cursor } } : {}) })
|
|
446
|
+
.sort({ seq: -1 }).limit(limit + 1).maxTimeMS(2000).toArray();
|
|
447
|
+
return inspectionPage(records, limit, 'seq', messageView);
|
|
448
|
+
}
|
|
425
449
|
async list(ownerId, collection, { cursor = null, limit = 20 }, filter = {}) {
|
|
450
|
+
if (!['messages', 'traces'].includes(collection)) fail('INVALID_REQUEST');
|
|
426
451
|
await this.getPerson(ownerId);
|
|
427
452
|
const docs = await this.collections[collection].find({ ...this.scope(ownerId), ...filter, ...(cursor ? { seq: { $lt: cursor } } : {}) }).sort({ seq: -1 }).limit(limit + 1).maxTimeMS(2000).toArray();
|
|
428
|
-
return { items: docs.slice(0, limit).map(collection === 'traces' ? publicTrace :
|
|
453
|
+
return { items: docs.slice(0, limit).map(collection === 'traces' ? publicTrace : messageView), nextCursor: docs.length > limit ? String(docs[limit - 1].seq) : null };
|
|
429
454
|
}
|
|
430
455
|
async snapshot(ownerId) {
|
|
431
456
|
await this.recover(ownerId);
|
|
@@ -438,7 +463,7 @@ export class MongoPersonRepository {
|
|
|
438
463
|
const concepts = await this.collections.concepts.find({ ...scope, id: { $in: state.focusConceptIds } }, { session }).limit(12).toArray();
|
|
439
464
|
const episode = await this.collections.episodes.findOne(scope, { session, sort: { inputWatermark: -1 },
|
|
440
465
|
projection: { _id: 0, id: 1, status: 1, terminalCode: 1, endedAt: 1 } });
|
|
441
|
-
return { latestEpisode: episode, person: this.personView(p), state:
|
|
466
|
+
return { latestEpisode: episode, person: this.personView(p), state: stateView(state), concepts: concepts.map(conceptView), messages: messages.slice(0, 20).reverse().map(messageView),
|
|
442
467
|
nextMessagesCursor: messages.length > 20 ? String(messages[19].seq) : null, busy: Boolean(p.activeEpisodeId), episodeId: p.activeEpisodeId };
|
|
443
468
|
});
|
|
444
469
|
}
|
package/yeaft/person/service.js
CHANGED
|
@@ -6,6 +6,7 @@ import { PersonRuntime } from './runtime.js';
|
|
|
6
6
|
import { validateFiles } from './attachments.js';
|
|
7
7
|
import { createPersonProvider, validateModelCandidates } from './provider.js';
|
|
8
8
|
import { fail, identifier, LIMITS, object, page, safeError, text } from './contracts.js';
|
|
9
|
+
import { inspectRequest, personName, searchRequest } from './inspection.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* One durable Person per (namespace, authenticated ownerId). The transport MUST
|
|
@@ -102,6 +103,12 @@ export function createPersonService(options = {}) {
|
|
|
102
103
|
}
|
|
103
104
|
case 'traces': await repository.recover(ownerId); return repository.list(ownerId, 'traces', page(payload));
|
|
104
105
|
case 'messages': return repository.list(ownerId, 'messages', page(payload));
|
|
106
|
+
// These reads deliberately bypass recover(), Recall and the runtime/provider.
|
|
107
|
+
case 'inspect': return repository.inspect(ownerId, inspectRequest(payload));
|
|
108
|
+
case 'search': {
|
|
109
|
+
searchRequest(payload);
|
|
110
|
+
return repository.search(ownerId, payload);
|
|
111
|
+
}
|
|
105
112
|
case 'cancel': {
|
|
106
113
|
object(payload, ['episodeId'], []);
|
|
107
114
|
if (payload.episodeId != null) identifier(payload.episodeId);
|
|
@@ -110,7 +117,9 @@ export function createPersonService(options = {}) {
|
|
|
110
117
|
return result;
|
|
111
118
|
}
|
|
112
119
|
case 'settings': {
|
|
113
|
-
object(payload, ['autonomyEnabled', 'modelCandidates'], []);
|
|
120
|
+
object(payload, ['name', 'autonomyEnabled', 'modelCandidates'], []);
|
|
121
|
+
const patch = { ...payload };
|
|
122
|
+
if (Object.hasOwn(patch, 'name')) patch.name = personName(patch.name);
|
|
114
123
|
if (Object.hasOwn(payload, 'autonomyEnabled') && typeof payload.autonomyEnabled !== 'boolean') fail('INVALID_REQUEST');
|
|
115
124
|
if (Object.hasOwn(payload, 'modelCandidates')) {
|
|
116
125
|
const refs = payload.modelCandidates;
|
|
@@ -122,7 +131,7 @@ export function createPersonService(options = {}) {
|
|
|
122
131
|
}
|
|
123
132
|
// Timer-driven autonomy is deliberately not claimed or silently enabled.
|
|
124
133
|
if (payload.autonomyEnabled === true) fail('UNSUPPORTED');
|
|
125
|
-
return repository.settings(ownerId,
|
|
134
|
+
return repository.settings(ownerId, patch);
|
|
126
135
|
}
|
|
127
136
|
default: fail('INVALID_REQUEST');
|
|
128
137
|
}
|
|
@@ -104,6 +104,8 @@ export class SqlitePersonRepository {
|
|
|
104
104
|
async settings(ownerId, settings) { return this.request('settings', [ownerId, settings]); }
|
|
105
105
|
async list(ownerId, collection, options, filter) { return this.request('list', [ownerId, collection, options, filter]); }
|
|
106
106
|
async snapshot(ownerId) { return this.request('snapshot', [ownerId]); }
|
|
107
|
+
async inspect(ownerId, options) { return this.request('inspect', [ownerId, options]); }
|
|
108
|
+
async search(ownerId, options) { return this.request('search', [ownerId, options]); }
|
|
107
109
|
/** Ascending durable journal, scoped before paging. Empty pages retain after. */
|
|
108
110
|
async searchChanges(ownerId, options) { return this.request('searchChanges', [ownerId, options]); }
|
|
109
111
|
/** kind is 'messages'|'concepts'; returns a deduplicated array of current public records. */
|
|
@@ -8,6 +8,8 @@ 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';
|
|
12
|
+
import { inspectCapabilities } from './capabilities.js';
|
|
11
13
|
|
|
12
14
|
const SCOPE = 'namespace = ? AND ownerId = ? AND personId = ?';
|
|
13
15
|
const scopeValues = s => [s.namespace, s.ownerId, s.personId];
|
|
@@ -46,7 +48,7 @@ const publicOutput = (output, failed) => {
|
|
|
46
48
|
return failed ? { text: output.text, retainedBytes, observedBytes, complete: false, accepted: false, availability: 'captured', usage, stopReason }
|
|
47
49
|
: { text: output.text, bytes: retainedBytes, complete: true, usage, stopReason };
|
|
48
50
|
};
|
|
49
|
-
const READS = new Set(['receipt', 'getPerson', 'context', 'recall', 'list', 'searchChanges', 'resolveMemories', 'createdCapabilities', 'episodeAttachments']);
|
|
51
|
+
const READS = new Set(['receipt', 'inspect', 'search', 'getPerson', 'context', 'recall', 'list', 'searchChanges', 'resolveMemories', 'createdCapabilities', 'episodeAttachments']);
|
|
50
52
|
const WRITES = new Set(['open', 'recover', 'admit', 'heartbeat', 'append', 'startCall', 'finalizeCall', 'commit', 'finish', 'cancel', 'settings', 'snapshot', 'saveCreatedCapability']);
|
|
51
53
|
const memoryKind = kind => { if (!['messages', 'concepts'].includes(kind)) fail('INVALID_REQUEST'); return kind; };
|
|
52
54
|
const boundedLimit = (limit, max = 100) => { if (!Number.isSafeInteger(limit) || limit < 1 || limit > max) fail('INVALID_REQUEST'); return limit; };
|
|
@@ -135,7 +137,7 @@ export class SqlitePersonStore {
|
|
|
135
137
|
.run(...scopeValues(scope), kind, record.id, record.revision, JSON.stringify(publicDoc(record)));
|
|
136
138
|
}
|
|
137
139
|
personView(p) {
|
|
138
|
-
return { id: p.personId, name: p.name, soul: p.soul, soulRevision: p.soulRevision, createdAt: p.createdAt.toISOString(), settings: p.settings };
|
|
140
|
+
return { id: p.personId, name: p.name, soul: p.soul, soulRevision: p.soulRevision, createdAt: p.createdAt.toISOString(), settings: settingsView(p.settings) };
|
|
139
141
|
}
|
|
140
142
|
getPerson(ownerId) {
|
|
141
143
|
const p = this.one('persons', this.scope(ownerId));
|
|
@@ -320,8 +322,8 @@ export class SqlitePersonStore {
|
|
|
320
322
|
const focused = this.focused(scope, state.focusConceptIds).slice(0, 12);
|
|
321
323
|
const recent = this.rows('concepts', scope, ' ORDER BY updatedAt DESC, id ASC LIMIT 12');
|
|
322
324
|
return { person: this.personView(p), capabilityExperience: capabilityExperienceView(p.capabilityExperience),
|
|
323
|
-
state:
|
|
324
|
-
concepts: [...new Map([...focused, ...recent].map(c => [c.id,
|
|
325
|
+
state: stateView(state), messages: this.rows('messages', scope, ' ORDER BY seq DESC LIMIT 12').reverse().map(messageView),
|
|
326
|
+
concepts: [...new Map([...focused, ...recent].map(c => [c.id, conceptView(c)])).values()] };
|
|
325
327
|
}
|
|
326
328
|
commit(episode, proposal, selection, callId, reportedSources = new Map()) {
|
|
327
329
|
const p = this.own(episode), scope = this.episodeScope(episode);
|
|
@@ -346,7 +348,7 @@ export class SqlitePersonStore {
|
|
|
346
348
|
if (this.one('states', scope)?.version !== episode.baseStateVersion) fail('STALE');
|
|
347
349
|
this.put('states', state);
|
|
348
350
|
this.put('state_commits', this.doc(scope, { id: randomUUID(), version: p.stateVersion, parentVersion: episode.baseStateVersion,
|
|
349
|
-
episodeId: episode.id, callId, conceptRevisions: revisions, state:
|
|
351
|
+
episodeId: episode.id, callId, conceptRevisions: revisions, state: stateView(state), createdAt: this.now }), true);
|
|
350
352
|
if (proposal.reply) {
|
|
351
353
|
const message = this.doc(scope, { id: randomUUID(), revision: 1, seq: p.messageSeq, episodeId: episode.id, role: 'assistant', text: proposal.reply, createdAt: this.now });
|
|
352
354
|
this.put('messages', message, true); this.journal(scope, 'messages', message);
|
|
@@ -355,7 +357,7 @@ export class SqlitePersonStore {
|
|
|
355
357
|
if (!record || record.status !== 'running') fail('STALE');
|
|
356
358
|
Object.assign(record, { status: 'completed', endedAt: this.now, stateVersion: p.stateVersion }); this.put('episodes', record);
|
|
357
359
|
this.trace(episode.ownerId, episode.id, 'committed', { callId, parentVersion: episode.baseStateVersion, stateVersion: p.stateVersion, conceptRevisions: revisions, decision: proposal.decision });
|
|
358
|
-
return { state:
|
|
360
|
+
return { state: stateView(state) };
|
|
359
361
|
}
|
|
360
362
|
finish(episode, status, code) {
|
|
361
363
|
const p = this.fenced(episode, false);
|
|
@@ -383,9 +385,26 @@ export class SqlitePersonStore {
|
|
|
383
385
|
settings(ownerId, settings) {
|
|
384
386
|
const p = this.getPerson(ownerId);
|
|
385
387
|
if (p.activeEpisodeId) fail('BUSY');
|
|
386
|
-
|
|
387
|
-
if (Object.
|
|
388
|
-
|
|
388
|
+
const { name, ...patch } = settings;
|
|
389
|
+
if (Object.hasOwn(settings, 'name')) p.name = personName(name);
|
|
390
|
+
p.settings = { ...settingsView(p.settings), ...patch }; p.controlVersion++; p.writeSerial++; this.put('persons', p);
|
|
391
|
+
if (Object.keys(settings).length) this.trace(ownerId, null, 'settings', { settings: p.settings, ...(name !== undefined ? { name: p.name } : {}) });
|
|
392
|
+
return { settings: settingsView(p.settings), person: this.personView(p) };
|
|
393
|
+
}
|
|
394
|
+
inspect(ownerId, options) {
|
|
395
|
+
const { section, cursor, limit } = inspectRequest(options);
|
|
396
|
+
this.getPerson(ownerId);
|
|
397
|
+
const scope = this.scope(ownerId);
|
|
398
|
+
if (section === 'skills') return inspectCapabilities(this.rows('created_capabilities', scope, ' ORDER BY id ASC LIMIT 32'), { cursor, limit });
|
|
399
|
+
const records = this.rows('concepts', scope, ' AND (? IS NULL OR id > ?) ORDER BY id ASC LIMIT ?', [cursor, cursor, limit + 1]);
|
|
400
|
+
return inspectionPage(records, limit, 'id', conceptView);
|
|
401
|
+
}
|
|
402
|
+
search(ownerId, options) {
|
|
403
|
+
const { query, cursor, limit } = searchRequest(options);
|
|
404
|
+
this.getPerson(ownerId);
|
|
405
|
+
const records = this.rows('messages', this.scope(ownerId),
|
|
406
|
+
' AND (? IS NULL OR seq < ?) AND instr(text, ?) > 0 ORDER BY seq DESC LIMIT ?', [cursor, cursor, query.toLowerCase(), limit + 1]);
|
|
407
|
+
return inspectionPage(records, limit, 'seq', messageView);
|
|
389
408
|
}
|
|
390
409
|
list(ownerId, collection, { cursor = null, limit = 20 } = {}, filter = {}) {
|
|
391
410
|
if (!['messages', 'traces'].includes(collection)) fail('INVALID_REQUEST');
|
|
@@ -398,7 +417,7 @@ export class SqlitePersonStore {
|
|
|
398
417
|
if (cursor != null) { clause += ' AND seq < ?'; params.push(sequence(cursor)); }
|
|
399
418
|
if (query) { clause += ' AND instr(text, ?) > 0'; params.push(query.toLowerCase()); }
|
|
400
419
|
const docs = this.rows(collection, this.scope(ownerId), `${clause} ORDER BY seq DESC LIMIT ?`, [...params, limit + 1]);
|
|
401
|
-
return { items: docs.slice(0, limit).map(publicDoc), nextCursor: docs.length > limit ? String(docs[limit - 1].seq) : null };
|
|
420
|
+
return { items: docs.slice(0, limit).map(collection === 'messages' ? messageView : publicDoc), nextCursor: docs.length > limit ? String(docs[limit - 1].seq) : null };
|
|
402
421
|
}
|
|
403
422
|
recall(ownerId, { kind = 'messages', query = '', cursor = null, limit = 5 } = {}) {
|
|
404
423
|
memoryKind(kind); text(query, LIMITS.inputBytes, true); boundedLimit(limit);
|
|
@@ -406,7 +425,7 @@ export class SqlitePersonStore {
|
|
|
406
425
|
this.getPerson(ownerId);
|
|
407
426
|
if (cursor != null) text(cursor, 128);
|
|
408
427
|
const docs = this.rows('concepts', this.scope(ownerId), ' AND (? IS NULL OR id > ?) AND instr(statement, ?) > 0 ORDER BY id ASC LIMIT ?', [cursor, cursor, query.toLowerCase(), limit + 1]);
|
|
409
|
-
return { items: docs.slice(0, limit).map(
|
|
428
|
+
return { items: docs.slice(0, limit).map(conceptView), nextCursor: docs.length > limit ? docs[limit - 1].id : null };
|
|
410
429
|
}
|
|
411
430
|
snapshot(ownerId) {
|
|
412
431
|
this.recover(ownerId);
|
|
@@ -415,15 +434,16 @@ export class SqlitePersonStore {
|
|
|
415
434
|
const episode = this.one('episodes', scope, ' ORDER BY inputWatermark DESC');
|
|
416
435
|
const latestEpisode = episode ? { id: episode.id, status: episode.status,
|
|
417
436
|
...(episode.terminalCode ? { terminalCode: episode.terminalCode } : {}), ...(episode.endedAt ? { endedAt: episode.endedAt } : {}) } : null;
|
|
418
|
-
return { latestEpisode, person: this.personView(p), state:
|
|
419
|
-
messages: messages.slice(0, 20).reverse().map(
|
|
437
|
+
return { latestEpisode, person: this.personView(p), state: stateView(state), concepts: this.focused(scope, state.focusConceptIds).slice(0, 12).map(conceptView),
|
|
438
|
+
messages: messages.slice(0, 20).reverse().map(messageView), nextMessagesCursor: messages.length > 20 ? String(messages[19].seq) : null,
|
|
420
439
|
busy: Boolean(p.activeEpisodeId), episodeId: p.activeEpisodeId };
|
|
421
440
|
}
|
|
422
441
|
searchChanges(ownerId, { after = 0, limit = 100 } = {}) {
|
|
423
442
|
this.getPerson(ownerId); after = sequence(after); boundedLimit(limit, 1000);
|
|
424
443
|
const rows = this.sql(`SELECT seq, kind, id, revision, record FROM memory_changes WHERE ${SCOPE} AND seq > ? ORDER BY seq ASC LIMIT ?`)
|
|
425
444
|
.all(...scopeValues(this.scope(ownerId)), after, limit + 1);
|
|
426
|
-
const items = rows.slice(0, limit).map(row => ({ ...row,
|
|
445
|
+
const items = rows.slice(0, limit).map(row => ({ ...row,
|
|
446
|
+
record: (row.kind === 'messages' ? messageView : conceptView)(decode(row, row.kind)) }));
|
|
427
447
|
return { items, lastSeq: items.at(-1)?.seq ?? after, hasMore: rows.length > limit };
|
|
428
448
|
}
|
|
429
449
|
resolveMemories(ownerId, kind, refs) {
|
|
@@ -433,7 +453,7 @@ export class SqlitePersonStore {
|
|
|
433
453
|
for (const ref of refs) {
|
|
434
454
|
if (!ref || typeof ref.id !== 'string' || !Number.isSafeInteger(ref.revision) || ref.revision < 1) fail('INVALID_REQUEST');
|
|
435
455
|
const record = this.one(kind, scope, ' AND id = ? AND revision = ?', [ref.id, ref.revision]);
|
|
436
|
-
if (record && !seen.has(record.id)) { seen.add(record.id); records.push(
|
|
456
|
+
if (record && !seen.has(record.id)) { seen.add(record.id); records.push((kind === 'messages' ? messageView : conceptView)(record)); }
|
|
437
457
|
}
|
|
438
458
|
return records;
|
|
439
459
|
}
|