@bizagent/dsh 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/LICENSE +21 -0
- package/README.md +61 -2
- package/cordis.patch.yml +14 -0
- package/docs/README.md +23 -0
- package/docs/architecture.md +154 -0
- package/docs/concepts.md +192 -0
- package/docs/configuration.md +154 -0
- package/docs/quickstart.md +103 -0
- package/docs/quickstart.zh-CN.md +86 -0
- package/docs/tools.md +607 -0
- package/docs/user-guide.zh-CN.md +132 -0
- package/docs/verification.md +69 -0
- package/examples/README.md +64 -0
- package/examples/demo-runtime.mjs +71 -0
- package/examples/learning-lab.mjs +263 -0
- package/examples/scenarios.mjs +45 -0
- package/lib/agent-preset.d.ts +20 -0
- package/lib/agent-preset.js +46 -0
- package/lib/agent-session-address.d.ts +14 -0
- package/lib/agent-session-address.js +15 -0
- package/lib/agent-session-coordinator.d.ts +24 -0
- package/lib/agent-session-coordinator.js +101 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +142 -0
- package/lib/client-types/agent-session-address.d.ts +13 -0
- package/lib/client-types/client/agent-mentions.d.ts +3 -0
- package/lib/client-types/client/api.d.ts +180 -0
- package/lib/client-types/client/components.d.ts +22 -0
- package/lib/client-types/client/controller.d.ts +23 -0
- package/lib/client-types/client/frame-participants.d.ts +5 -0
- package/lib/client-types/client/home-name.d.ts +9 -0
- package/lib/client-types/client/human-inbox.d.ts +8 -0
- package/lib/client-types/client/index.d.ts +13 -0
- package/lib/client-types/client/locales.d.ts +796 -0
- package/lib/client-types/client/markdown.d.ts +12 -0
- package/lib/client-types/client/project-artifact-link.d.ts +10 -0
- package/lib/client-types/client/project-outcome-review-inbox.d.ts +13 -0
- package/lib/client-types/client/projects.d.ts +11 -0
- package/lib/client-types/client/sample-guide.d.ts +9 -0
- package/lib/client-types/client/session-resource.d.ts +34 -0
- package/lib/client-types/client/session-surfaces.d.ts +24 -0
- package/lib/client-types/client/styles.d.ts +1 -0
- package/lib/client-types/config.d.ts +55 -0
- package/lib/client-types/cron.d.ts +8 -0
- package/lib/client-types/domain.d.ts +818 -0
- package/lib/client-types/frame-reference.d.ts +4 -0
- package/lib/client-types/home-directory.d.ts +5 -0
- package/lib/client-types/home-space.d.ts +49 -0
- package/lib/client-types/home-store.d.ts +125 -0
- package/lib/client-types/inbox-stalled-topic.d.ts +15 -0
- package/lib/client-types/inbox-wake-policy.d.ts +18 -0
- package/lib/client-types/known-format.d.ts +2 -0
- package/lib/client-types/learning-scheduler.d.ts +36 -0
- package/lib/client-types/learning-session-runner.d.ts +49 -0
- package/lib/client-types/learning-trigger.d.ts +17 -0
- package/lib/client-types/learning-usage.d.ts +17 -0
- package/lib/client-types/ledger.d.ts +190 -0
- package/lib/client-types/local-owner.d.ts +7 -0
- package/lib/client-types/message-source.d.ts +9 -0
- package/lib/client-types/organization-location.d.ts +19 -0
- package/lib/client-types/project-ledger.d.ts +182 -0
- package/lib/client-types/project-seat-context.d.ts +17 -0
- package/lib/client-types/safe-file.d.ts +23 -0
- package/lib/client-types/sample-organization.d.ts +9 -0
- package/lib/client-types/service.d.ts +596 -0
- package/lib/client-types/shared-knowledge.d.ts +116 -0
- package/lib/client-types/ui-contract.d.ts +276 -0
- package/lib/client-types/untrusted-text.d.ts +15 -0
- package/lib/client-types/work-ledger.d.ts +108 -0
- package/lib/client-types/work-output-eval.d.ts +15 -0
- package/lib/client-types/worklog-ledger.d.ts +88 -0
- package/lib/client-types/worklog-snapshot.d.ts +33 -0
- package/lib/client.js +5463 -0
- package/lib/config.d.ts +56 -0
- package/lib/config.js +115 -0
- package/lib/cron.d.ts +9 -0
- package/lib/cron.js +127 -0
- package/lib/domain.d.ts +819 -0
- package/lib/domain.js +662 -0
- package/lib/frame-admission.d.ts +6 -0
- package/lib/frame-admission.js +39 -0
- package/lib/frame-reference.d.ts +5 -0
- package/lib/frame-reference.js +12 -0
- package/lib/home-archive.d.ts +17 -0
- package/lib/home-archive.js +177 -0
- package/lib/home-directory.d.ts +6 -0
- package/lib/home-directory.js +38 -0
- package/lib/home-space.d.ts +50 -0
- package/lib/home-space.js +120 -0
- package/lib/home-store.d.ts +126 -0
- package/lib/home-store.js +837 -0
- package/lib/host.d.ts +22 -0
- package/lib/host.js +15 -0
- package/lib/human-inbox-transport.d.ts +23 -0
- package/lib/human-inbox-transport.js +105 -0
- package/lib/human-notification-runner.d.ts +83 -0
- package/lib/human-notification-runner.js +138 -0
- package/lib/human-notification-webhook.d.ts +18 -0
- package/lib/human-notification-webhook.js +102 -0
- package/lib/human-turn-completion.d.ts +61 -0
- package/lib/human-turn-completion.js +157 -0
- package/lib/inbox-return-notifier.d.ts +46 -0
- package/lib/inbox-return-notifier.js +279 -0
- package/lib/inbox-stalled-escalation-runner.d.ts +31 -0
- package/lib/inbox-stalled-escalation-runner.js +68 -0
- package/lib/inbox-stalled-topic.d.ts +16 -0
- package/lib/inbox-stalled-topic.js +44 -0
- package/lib/inbox-wake-policy.d.ts +19 -0
- package/lib/inbox-wake-policy.js +28 -0
- package/lib/inbox-wake-runner.d.ts +45 -0
- package/lib/inbox-wake-runner.js +481 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.js +480 -0
- package/lib/known-format.d.ts +3 -0
- package/lib/known-format.js +8 -0
- package/lib/learning-scheduler.d.ts +37 -0
- package/lib/learning-scheduler.js +182 -0
- package/lib/learning-session-runner.d.ts +50 -0
- package/lib/learning-session-runner.js +435 -0
- package/lib/learning-trigger.d.ts +18 -0
- package/lib/learning-trigger.js +92 -0
- package/lib/learning-usage.d.ts +18 -0
- package/lib/learning-usage.js +49 -0
- package/lib/ledger.d.ts +191 -0
- package/lib/ledger.js +752 -0
- package/lib/local-human-host.d.ts +11 -0
- package/lib/local-human-host.js +191 -0
- package/lib/local-human-token.d.ts +9 -0
- package/lib/local-human-token.js +32 -0
- package/lib/local-owner.d.ts +8 -0
- package/lib/local-owner.js +26 -0
- package/lib/message-source.d.ts +10 -0
- package/lib/message-source.js +2 -0
- package/lib/organization-location.d.ts +20 -0
- package/lib/organization-location.js +80 -0
- package/lib/organization-workspace.d.ts +22 -0
- package/lib/organization-workspace.js +32 -0
- package/lib/project-ledger.d.ts +183 -0
- package/lib/project-ledger.js +189 -0
- package/lib/project-review-transport.d.ts +23 -0
- package/lib/project-review-transport.js +155 -0
- package/lib/project-seat-context.d.ts +18 -0
- package/lib/project-seat-context.js +34 -0
- package/lib/proposal-review-transport.d.ts +26 -0
- package/lib/proposal-review-transport.js +104 -0
- package/lib/route-guard.d.ts +31 -0
- package/lib/route-guard.js +80 -0
- package/lib/safe-file.d.ts +24 -0
- package/lib/safe-file.js +93 -0
- package/lib/sample-organization.d.ts +10 -0
- package/lib/sample-organization.js +46 -0
- package/lib/service.d.ts +597 -0
- package/lib/service.js +4076 -0
- package/lib/session-surface.d.ts +11 -0
- package/lib/session-surface.js +109 -0
- package/lib/shared-knowledge-review-transport.d.ts +28 -0
- package/lib/shared-knowledge-review-transport.js +85 -0
- package/lib/shared-knowledge.d.ts +117 -0
- package/lib/shared-knowledge.js +240 -0
- package/lib/tools.d.ts +7 -0
- package/lib/tools.js +955 -0
- package/lib/ui-contract.d.ts +277 -0
- package/lib/ui-contract.js +12 -0
- package/lib/ui-host.d.ts +24 -0
- package/lib/ui-host.js +471 -0
- package/lib/untrusted-text.d.ts +16 -0
- package/lib/untrusted-text.js +50 -0
- package/lib/work-ledger.d.ts +109 -0
- package/lib/work-ledger.js +107 -0
- package/lib/work-output-eval.d.ts +16 -0
- package/lib/work-output-eval.js +58 -0
- package/lib/work-runner.d.ts +47 -0
- package/lib/work-runner.js +352 -0
- package/lib/worklog-ledger.d.ts +89 -0
- package/lib/worklog-ledger.js +91 -0
- package/lib/worklog-snapshot.d.ts +34 -0
- package/lib/worklog-snapshot.js +100 -0
- package/lib/writer-guard.d.ts +11 -0
- package/lib/writer-guard.js +207 -0
- package/package.json +172 -4
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { lstat, mkdir, readdir, realpath, rename, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { AgentHomeSchema, AssetMetadataSchema, OrganizationSchema, homeTypeOf, nowIso, parseHomeAddress, safeHomeId, } from './domain.js';
|
|
7
|
+
import { DEFAULT_INBOX_WAKE_POLICY, InboxWakePolicySchema } from './inbox-wake-policy.js';
|
|
8
|
+
import { readRegularFileNoFollow, writeFileDurably } from './safe-file.js';
|
|
9
|
+
import { assertKnownFormat } from './known-format.js';
|
|
10
|
+
const DIRECTORY_SCHEMA = z.object({
|
|
11
|
+
schemaVersion: z.literal(1),
|
|
12
|
+
homes: z.array(z.object({
|
|
13
|
+
id: z.string().min(1),
|
|
14
|
+
address: z.string().min(1),
|
|
15
|
+
path: z.string().min(1),
|
|
16
|
+
/** Owner recorded by this store; null records "no owner". Absent in directories written before 1.0. */
|
|
17
|
+
owner: z.string().min(1).nullable().optional(),
|
|
18
|
+
})),
|
|
19
|
+
});
|
|
20
|
+
const KIND_DIRECTORY = {
|
|
21
|
+
memory: 'memory',
|
|
22
|
+
insight: 'insights',
|
|
23
|
+
knowledge: 'knowledge',
|
|
24
|
+
method: 'methods',
|
|
25
|
+
};
|
|
26
|
+
export class HomeStore {
|
|
27
|
+
root;
|
|
28
|
+
homesRoot;
|
|
29
|
+
organizationFile;
|
|
30
|
+
homes = new Map();
|
|
31
|
+
organization;
|
|
32
|
+
organizationLoadError;
|
|
33
|
+
integrityIssues = [];
|
|
34
|
+
mutationTail = Promise.resolve();
|
|
35
|
+
constructor(root) {
|
|
36
|
+
this.root = resolve(root);
|
|
37
|
+
this.homesRoot = join(this.root, 'homes');
|
|
38
|
+
this.organizationFile = join(this.root, 'organization.yaml');
|
|
39
|
+
}
|
|
40
|
+
async initialize() {
|
|
41
|
+
if (!isAbsolute(this.root))
|
|
42
|
+
throw new Error(`BizAgent home root must be absolute: ${this.root}`);
|
|
43
|
+
await mkdir(this.homesRoot, { recursive: true });
|
|
44
|
+
const actualRoot = await realpath(this.root);
|
|
45
|
+
if (actualRoot !== this.root) {
|
|
46
|
+
throw new Error(`BizAgent home root may not be a symlink: ${this.root} -> ${actualRoot}`);
|
|
47
|
+
}
|
|
48
|
+
await this.reload();
|
|
49
|
+
}
|
|
50
|
+
async reload() {
|
|
51
|
+
this.integrityIssues = [];
|
|
52
|
+
const recordedOwners = await this.readRecordedOwners();
|
|
53
|
+
this.homes.clear();
|
|
54
|
+
this.organization = undefined;
|
|
55
|
+
const entries = await readdir(this.homesRoot, { withFileTypes: true });
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (!entry.isDirectory())
|
|
58
|
+
continue;
|
|
59
|
+
if (entry.name.endsWith('.tmp'))
|
|
60
|
+
continue;
|
|
61
|
+
const directory = join(this.homesRoot, entry.name);
|
|
62
|
+
let cache;
|
|
63
|
+
try {
|
|
64
|
+
cache = await this.loadHome(directory);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// A Home that fails to load or validate stays unavailable, but must not take the other
|
|
68
|
+
// Homes down with it; doctor and health report why it was skipped.
|
|
69
|
+
this.integrityIssues.push(`Agent Home skipped: ${this.relativePath(directory)}: ${this.relativeMessage(error)}`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (this.homes.has(cache.home.address)) {
|
|
73
|
+
this.integrityIssues.push(`Agent Home skipped: duplicate address ${cache.home.address} at ${this.relativePath(directory)}`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
this.keepRecordedOwner(cache, recordedOwners);
|
|
77
|
+
this.homes.set(cache.home.address, cache);
|
|
78
|
+
}
|
|
79
|
+
await this.loadOrganization();
|
|
80
|
+
await this.writeDirectory();
|
|
81
|
+
}
|
|
82
|
+
listHomes() {
|
|
83
|
+
return [...this.homes.values()]
|
|
84
|
+
.map(cache => structuredClone(cache.home))
|
|
85
|
+
.sort((a, b) => a.address.localeCompare(b.address));
|
|
86
|
+
}
|
|
87
|
+
hasHome(address) {
|
|
88
|
+
try {
|
|
89
|
+
return this.homes.has(parseHomeAddress(address));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
getHome(address) {
|
|
96
|
+
return structuredClone(this.requireHome(parseHomeAddress(address)).home);
|
|
97
|
+
}
|
|
98
|
+
/** Any loaded Home, including an archived one, or undefined; never throws for an unknown address. */
|
|
99
|
+
findHome(address) {
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = parseHomeAddress(address);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const cache = this.homes.get(parsed);
|
|
108
|
+
return cache === undefined ? undefined : structuredClone(cache.home);
|
|
109
|
+
}
|
|
110
|
+
/** The Home's fixed DSH Workspace, shared by Principal, Project Main and Seats. */
|
|
111
|
+
homeWorkspacePath(address) {
|
|
112
|
+
return this.requireHome(parseHomeAddress(address)).directory;
|
|
113
|
+
}
|
|
114
|
+
/** Home-local receipt policy. Missing or malformed files use the 15-minute batch default. */
|
|
115
|
+
async readInboxWakePolicy(address) {
|
|
116
|
+
const file = join(this.requireHome(parseHomeAddress(address)).directory, 'inbox.json');
|
|
117
|
+
try {
|
|
118
|
+
const bytes = await readRegularFileNoFollow(file, { maxBytes: 4096 });
|
|
119
|
+
return InboxWakePolicySchema.parse(JSON.parse(bytes.toString('utf8')));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return DEFAULT_INBOX_WAKE_POLICY;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
getIdentity(address) {
|
|
126
|
+
return this.requireHome(parseHomeAddress(address)).identity;
|
|
127
|
+
}
|
|
128
|
+
getOrganization() {
|
|
129
|
+
return this.organization === undefined ? undefined : structuredClone(this.organization);
|
|
130
|
+
}
|
|
131
|
+
async createHome(input) {
|
|
132
|
+
return this.mutate(async () => this.createHomeUnlocked(input, true, false));
|
|
133
|
+
}
|
|
134
|
+
async createOrganization(input) {
|
|
135
|
+
return this.mutate(async () => {
|
|
136
|
+
if (this.organization !== undefined) {
|
|
137
|
+
if (this.organization.id === input.id)
|
|
138
|
+
return structuredClone(this.organization);
|
|
139
|
+
throw new Error(`organization already exists: ${this.organization.id}`);
|
|
140
|
+
}
|
|
141
|
+
if (this.organizationLoadError !== undefined) {
|
|
142
|
+
throw new Error('organization.yaml exists but could not be loaded; fix or move it before creating an organization');
|
|
143
|
+
}
|
|
144
|
+
const id = normalizeId(input.id, 'organization');
|
|
145
|
+
const name = normalizeLabel(input.name, 'organization name');
|
|
146
|
+
const mission = normalizeBody(input.mission);
|
|
147
|
+
if (input.members.length === 0)
|
|
148
|
+
throw new Error('organization must have at least one member');
|
|
149
|
+
const lineHome = parseHomeAddress(`line:${id}`);
|
|
150
|
+
const businessHome = parseHomeAddress(`business:${id}`);
|
|
151
|
+
const lineOwner = input.owner ?? normalizeId(input.members[0].id, 'line owner');
|
|
152
|
+
const memberHomes = [];
|
|
153
|
+
const capabilityHomes = [];
|
|
154
|
+
const links = [];
|
|
155
|
+
const homeInputs = [{
|
|
156
|
+
address: lineHome,
|
|
157
|
+
displayName: `${name} PMO`,
|
|
158
|
+
owner: lineOwner,
|
|
159
|
+
identity: `${name} product line PMO. Coordinate its enduring organization and Projects.\n\nMission: ${mission}`,
|
|
160
|
+
}, {
|
|
161
|
+
address: businessHome,
|
|
162
|
+
displayName: name,
|
|
163
|
+
owner: lineOwner,
|
|
164
|
+
identity: `${name} exists to ${mission}\n\nThis Business Home owns shared goals, decisions, and organizational learning.`,
|
|
165
|
+
}];
|
|
166
|
+
const addresses = new Set([lineHome, businessHome]);
|
|
167
|
+
links.push({ from: businessHome, to: lineHome, relation: 'belongs-to-line' });
|
|
168
|
+
const roles = new Map();
|
|
169
|
+
for (const member of input.members) {
|
|
170
|
+
const memberId = normalizeId(member.id, 'member');
|
|
171
|
+
const roleId = normalizeId(member.roleId, 'role');
|
|
172
|
+
const displayName = normalizeLabel(member.displayName, 'member name');
|
|
173
|
+
const roleName = normalizeLabel(member.roleName, 'role name');
|
|
174
|
+
const responsibility = normalizeBody(member.responsibility);
|
|
175
|
+
const personalHome = parseHomeAddress(`personal:${memberId}`);
|
|
176
|
+
const roleHome = parseHomeAddress(`role:${roleId}`);
|
|
177
|
+
if (addresses.has(personalHome))
|
|
178
|
+
throw new Error(`duplicate member id: ${memberId}`);
|
|
179
|
+
addresses.add(personalHome);
|
|
180
|
+
const existingRole = roles.get(roleId);
|
|
181
|
+
if (existingRole !== undefined
|
|
182
|
+
&& (existingRole.name !== roleName || existingRole.responsibility !== responsibility)) {
|
|
183
|
+
throw new Error(`role ${roleId} has conflicting definitions`);
|
|
184
|
+
}
|
|
185
|
+
if (existingRole === undefined) {
|
|
186
|
+
roles.set(roleId, { name: roleName, responsibility });
|
|
187
|
+
addresses.add(roleHome);
|
|
188
|
+
homeInputs.push({
|
|
189
|
+
address: roleHome,
|
|
190
|
+
displayName: roleName,
|
|
191
|
+
owner: lineOwner,
|
|
192
|
+
identity: `${roleName} serves ${name}.\n\nEnduring responsibility: ${responsibility}`,
|
|
193
|
+
});
|
|
194
|
+
links.push({ from: roleHome, to: businessHome, relation: 'serves' });
|
|
195
|
+
links.push({ from: roleHome, to: lineHome, relation: 'belongs-to-line' });
|
|
196
|
+
}
|
|
197
|
+
homeInputs.push({
|
|
198
|
+
address: personalHome,
|
|
199
|
+
displayName,
|
|
200
|
+
owner: input.owner ?? memberId,
|
|
201
|
+
identity: `${displayName} is a member of ${name} and fulfills the ${roleName} role.\n\nResponsibility: ${responsibility}`,
|
|
202
|
+
});
|
|
203
|
+
memberHomes.push({ personalHome, roleHome });
|
|
204
|
+
links.push({ from: personalHome, to: businessHome, relation: 'member-of' }, { from: personalHome, to: roleHome, relation: 'fulfills-role' });
|
|
205
|
+
}
|
|
206
|
+
for (const capability of input.capabilities) {
|
|
207
|
+
const capabilityId = normalizeId(capability.id, 'capability');
|
|
208
|
+
const displayName = normalizeLabel(capability.displayName, 'capability name');
|
|
209
|
+
const responsibility = normalizeBody(capability.responsibility);
|
|
210
|
+
const capabilityHome = parseHomeAddress(`capability:${capabilityId}`);
|
|
211
|
+
if (addresses.has(capabilityHome))
|
|
212
|
+
throw new Error(`duplicate capability id: ${capabilityId}`);
|
|
213
|
+
addresses.add(capabilityHome);
|
|
214
|
+
capabilityHomes.push(capabilityHome);
|
|
215
|
+
homeInputs.push({
|
|
216
|
+
address: capabilityHome,
|
|
217
|
+
displayName,
|
|
218
|
+
owner: lineOwner,
|
|
219
|
+
identity: `${displayName} is a reusable organizational capability of ${name}.\n\nIt owns: ${responsibility}`,
|
|
220
|
+
});
|
|
221
|
+
links.push({ from: capabilityHome, to: businessHome, relation: 'serves' });
|
|
222
|
+
links.push({ from: capabilityHome, to: lineHome, relation: 'belongs-to-line' });
|
|
223
|
+
}
|
|
224
|
+
// Homes are idempotent, so a failed setup can be retried safely before the
|
|
225
|
+
// manifest is committed. The organization becomes visible only after every
|
|
226
|
+
// referenced Home exists.
|
|
227
|
+
for (const homeInput of homeInputs)
|
|
228
|
+
await this.createHomeUnlocked(homeInput, false, true);
|
|
229
|
+
const createdAt = nowIso();
|
|
230
|
+
const organization = OrganizationSchema.parse({
|
|
231
|
+
schemaVersion: 1,
|
|
232
|
+
id,
|
|
233
|
+
name,
|
|
234
|
+
mission,
|
|
235
|
+
lineHome,
|
|
236
|
+
businessHome,
|
|
237
|
+
members: memberHomes,
|
|
238
|
+
capabilityHomes,
|
|
239
|
+
links,
|
|
240
|
+
revision: 1,
|
|
241
|
+
createdAt,
|
|
242
|
+
updatedAt: createdAt,
|
|
243
|
+
});
|
|
244
|
+
await atomicWrite(this.organizationFile, YAML.stringify(organization, { lineWidth: 0 }));
|
|
245
|
+
this.organization = organization;
|
|
246
|
+
await this.writeDirectory();
|
|
247
|
+
return structuredClone(organization);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
listAssets(address, status) {
|
|
251
|
+
const cache = this.requireHome(parseHomeAddress(address));
|
|
252
|
+
return [...cache.assets.values()]
|
|
253
|
+
.filter(asset => status === undefined || asset.status === status)
|
|
254
|
+
.map(asset => structuredClone(asset))
|
|
255
|
+
.sort(compareAssets);
|
|
256
|
+
}
|
|
257
|
+
searchAssets(address, input = {}) {
|
|
258
|
+
const cache = this.requireHome(parseHomeAddress(address));
|
|
259
|
+
const query = input.query?.trim().toLocaleLowerCase();
|
|
260
|
+
const limit = Math.min(Math.max(input.limit ?? 20, 1), 100);
|
|
261
|
+
return [...cache.assets.values()]
|
|
262
|
+
.filter(asset => input.kind === undefined || asset.kind === input.kind)
|
|
263
|
+
.filter(asset => input.status === undefined ? asset.status === 'active' : asset.status === input.status)
|
|
264
|
+
.filter((asset) => {
|
|
265
|
+
if (!query)
|
|
266
|
+
return true;
|
|
267
|
+
const haystack = [asset.id, asset.title, asset.description, asset.tags.join(' '), asset.body]
|
|
268
|
+
.filter((value) => typeof value === 'string')
|
|
269
|
+
.join('\n')
|
|
270
|
+
.toLocaleLowerCase();
|
|
271
|
+
return haystack.includes(query);
|
|
272
|
+
})
|
|
273
|
+
.sort(compareAssets)
|
|
274
|
+
.slice(0, limit)
|
|
275
|
+
.map(asset => structuredClone(asset));
|
|
276
|
+
}
|
|
277
|
+
readAsset(address, id) {
|
|
278
|
+
const asset = this.requireHome(parseHomeAddress(address)).assets.get(id);
|
|
279
|
+
if (asset === undefined)
|
|
280
|
+
throw new Error(`asset not found in ${address}: ${id}`);
|
|
281
|
+
return structuredClone(asset);
|
|
282
|
+
}
|
|
283
|
+
findAssetByProposalId(address, proposalId) {
|
|
284
|
+
const cache = this.requireHome(parseHomeAddress(address));
|
|
285
|
+
const found = [...cache.assets.values()].find(asset => asset.proposalId === proposalId
|
|
286
|
+
|| asset.proposalApplications?.some(application => application.proposalId === proposalId));
|
|
287
|
+
return found === undefined ? undefined : structuredClone(found);
|
|
288
|
+
}
|
|
289
|
+
async createAsset(input) {
|
|
290
|
+
return this.mutate(async () => {
|
|
291
|
+
const cache = this.requireHome(input.ownerAddress);
|
|
292
|
+
const existing = cache.assets.get(input.id);
|
|
293
|
+
if (existing !== undefined) {
|
|
294
|
+
if ((input.proposalId !== undefined && existing.proposalId === input.proposalId)
|
|
295
|
+
|| (existing.kind === input.kind
|
|
296
|
+
&& existing.description === input.description.trim()
|
|
297
|
+
&& existing.body === normalizeBody(input.body)
|
|
298
|
+
&& JSON.stringify(existing.tags) === JSON.stringify(normalizeTags(input.tags ?? [])))) {
|
|
299
|
+
return structuredClone(existing);
|
|
300
|
+
}
|
|
301
|
+
throw new Error(`asset id collision in ${input.ownerAddress}: ${input.id}`);
|
|
302
|
+
}
|
|
303
|
+
const now = nowIso();
|
|
304
|
+
const body = normalizeBody(input.body);
|
|
305
|
+
const metadata = AssetMetadataSchema.parse({
|
|
306
|
+
schemaVersion: 1,
|
|
307
|
+
id: input.id,
|
|
308
|
+
ownerAddress: input.ownerAddress,
|
|
309
|
+
kind: input.kind,
|
|
310
|
+
...(input.title?.trim() ? { title: input.title.trim() } : {}),
|
|
311
|
+
description: input.description.trim(),
|
|
312
|
+
tags: normalizeTags(input.tags ?? []),
|
|
313
|
+
sourceRefs: input.sourceRefs,
|
|
314
|
+
confidence: input.confidence ?? 0.7,
|
|
315
|
+
fitness: input.fitness ?? 0,
|
|
316
|
+
status: input.status ?? 'active',
|
|
317
|
+
revision: 1,
|
|
318
|
+
...(input.proposalId !== undefined ? { proposalId: input.proposalId } : {}),
|
|
319
|
+
...(input.proposalId !== undefined ? { proposalApplications: [{ proposalId: input.proposalId,
|
|
320
|
+
revision: 1, bodySha256: createHash('sha256').update(body).digest('hex') }] } : {}),
|
|
321
|
+
createdAt: now,
|
|
322
|
+
updatedAt: now,
|
|
323
|
+
});
|
|
324
|
+
const bodyPath = this.assetPath(cache, input.kind, metadata.id);
|
|
325
|
+
const stored = { ...metadata, body, bodyPath };
|
|
326
|
+
await atomicWrite(bodyPath, renderAsset(stored));
|
|
327
|
+
cache.assets.set(stored.id, stored);
|
|
328
|
+
await this.refreshHome(cache);
|
|
329
|
+
return structuredClone(stored);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
async updateAsset(address, id, input) {
|
|
333
|
+
return this.mutate(async () => {
|
|
334
|
+
const cache = this.requireHome(parseHomeAddress(address));
|
|
335
|
+
const current = cache.assets.get(id);
|
|
336
|
+
if (current === undefined)
|
|
337
|
+
throw new Error(`asset not found in ${address}: ${id}`);
|
|
338
|
+
if (input.expectedRevision !== undefined
|
|
339
|
+
&& (!Number.isInteger(input.expectedRevision) || input.expectedRevision < 1
|
|
340
|
+
|| current.revision !== input.expectedRevision)) {
|
|
341
|
+
throw new Error(`asset revision conflict in ${address}: ${id}; expected ${input.expectedRevision}, current ${current.revision}. Read the asset again before updating it.`);
|
|
342
|
+
}
|
|
343
|
+
if (input.appliedProposalId !== undefined && (!input.appliedProposalId.trim()
|
|
344
|
+
|| current.proposalId === input.appliedProposalId
|
|
345
|
+
|| current.proposalApplications?.some(item => item.proposalId === input.appliedProposalId))) {
|
|
346
|
+
throw new Error(`asset proposal application already exists or is invalid: ${input.appliedProposalId}`);
|
|
347
|
+
}
|
|
348
|
+
const body = input.body === undefined ? current.body : normalizeBody(input.body);
|
|
349
|
+
const updatedAt = nowIso();
|
|
350
|
+
const metadata = AssetMetadataSchema.parse({
|
|
351
|
+
...stripStoredFields(current),
|
|
352
|
+
...(input.description !== undefined ? { description: input.description.trim() } : {}),
|
|
353
|
+
...(input.tags !== undefined ? { tags: normalizeTags(input.tags) } : {}),
|
|
354
|
+
...(input.sourceRefs !== undefined ? { sourceRefs: input.sourceRefs } : {}),
|
|
355
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
356
|
+
...(input.confidence !== undefined ? { confidence: input.confidence } : {}),
|
|
357
|
+
...(input.fitness !== undefined ? { fitness: input.fitness } : {}),
|
|
358
|
+
...(input.appliedProposalId === undefined ? {} : { proposalApplications: [
|
|
359
|
+
...(current.proposalApplications ?? []), { proposalId: input.appliedProposalId,
|
|
360
|
+
revision: current.revision + 1,
|
|
361
|
+
bodySha256: createHash('sha256').update(body).digest('hex') },
|
|
362
|
+
] }),
|
|
363
|
+
revision: current.revision + 1,
|
|
364
|
+
updatedAt,
|
|
365
|
+
});
|
|
366
|
+
const stored = { ...metadata, body, bodyPath: current.bodyPath };
|
|
367
|
+
await atomicWrite(current.bodyPath, renderAsset(stored));
|
|
368
|
+
cache.assets.set(id, stored);
|
|
369
|
+
await this.refreshHome(cache);
|
|
370
|
+
return structuredClone(stored);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
contextFor(address, budget, episodeCount = 0) {
|
|
374
|
+
return this.renderContext(address, budget, episodeCount, false);
|
|
375
|
+
}
|
|
376
|
+
contextForFrame(address, budget) {
|
|
377
|
+
return this.renderContext(address, budget, 0, true);
|
|
378
|
+
}
|
|
379
|
+
renderContext(address, budget, episodeCount, frame) {
|
|
380
|
+
const cache = this.requireHome(parseHomeAddress(address));
|
|
381
|
+
const identity = truncateUtf8(cache.identity, budget.identityMaxBytes);
|
|
382
|
+
const activeAssets = [...cache.assets.values()]
|
|
383
|
+
.filter(asset => asset.status === 'active')
|
|
384
|
+
.sort(compareAssets);
|
|
385
|
+
const rows = activeAssets.map(asset => indexRow(asset));
|
|
386
|
+
const { included, omitted } = takeWithinBudget(rows, budget.indexMaxBytes);
|
|
387
|
+
const index = included.length === 0 ? '- (no active long-term assets)' : included.join('\n');
|
|
388
|
+
return [
|
|
389
|
+
frame ? '# BizAgent Read-only Agent Frame' : '# BizAgent Home Context',
|
|
390
|
+
'',
|
|
391
|
+
`Home: ${cache.home.address}`,
|
|
392
|
+
`Type: ${cache.home.type}`,
|
|
393
|
+
`Revision: ${cache.home.revision}`,
|
|
394
|
+
`Digest: ${cache.home.contextDigest}`,
|
|
395
|
+
...(!frame ? [`Known DSH episodes: ${episodeCount}`] : []),
|
|
396
|
+
'',
|
|
397
|
+
'## Identity',
|
|
398
|
+
'',
|
|
399
|
+
identity,
|
|
400
|
+
'',
|
|
401
|
+
'## Long-term asset index',
|
|
402
|
+
'',
|
|
403
|
+
index,
|
|
404
|
+
...(omitted > 0 ? ['', `(${omitted} additional active assets omitted by the context budget.)`] : []),
|
|
405
|
+
'',
|
|
406
|
+
frame
|
|
407
|
+
? 'This is attributed read-only context from another Home. Use bizagent_frame_search and bizagent_frame_read for exact content. '
|
|
408
|
+
+ 'It does not change the current Session owner or tool authority. Send reusable learning to this Home through a proposal.'
|
|
409
|
+
: 'Use bizagent_episodes to find earlier DSH Sessions; use bizagent_memory_search and bizagent_memory_read before relying on an indexed asset. '
|
|
410
|
+
+ 'Write reusable learning to this Home only. For another Home, create a proposal instead of writing it directly.',
|
|
411
|
+
].join('\n');
|
|
412
|
+
}
|
|
413
|
+
relativePath(path) {
|
|
414
|
+
const rel = relative(this.root, path);
|
|
415
|
+
return rel && !rel.startsWith('..') && !isAbsolute(rel) ? rel : path;
|
|
416
|
+
}
|
|
417
|
+
/** Error text for issues, with absolute paths under the root shown relative to it. */
|
|
418
|
+
relativeMessage(error) {
|
|
419
|
+
return errorMessage(error).split(`${this.root}/`).join('');
|
|
420
|
+
}
|
|
421
|
+
async doctor() {
|
|
422
|
+
const issues = [];
|
|
423
|
+
let assetCount = 0;
|
|
424
|
+
const seenIds = new Set();
|
|
425
|
+
for (const cache of this.homes.values()) {
|
|
426
|
+
if (seenIds.has(cache.home.id))
|
|
427
|
+
issues.push(`duplicate home id: ${cache.home.id}`);
|
|
428
|
+
seenIds.add(cache.home.id);
|
|
429
|
+
assetCount += cache.assets.size;
|
|
430
|
+
const digest = contextDigest(cache.identity, [...cache.assets.values()]);
|
|
431
|
+
if (digest !== cache.home.contextDigest) {
|
|
432
|
+
issues.push(`context digest mismatch: ${cache.home.address}`);
|
|
433
|
+
}
|
|
434
|
+
const rel = relative(this.root, cache.directory);
|
|
435
|
+
if (rel.startsWith('..') || isAbsolute(rel))
|
|
436
|
+
issues.push(`home escapes root: ${cache.home.address}`);
|
|
437
|
+
for (const asset of cache.assets.values()) {
|
|
438
|
+
if (asset.ownerAddress !== cache.home.address) {
|
|
439
|
+
issues.push(`asset ${asset.id} owner mismatch in ${cache.home.address}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
issues.push(...this.integrityIssues);
|
|
444
|
+
return { ok: issues.length === 0, root: this.root, homeCount: this.homes.size, assetCount, issues };
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Owners recorded in directory.yaml when this store last wrote it. The file sits at the store
|
|
448
|
+
* root, outside each Home's own Workspace directory, so an Agent working in its Home cannot
|
|
449
|
+
* change its owner by editing home.yaml alone.
|
|
450
|
+
*/
|
|
451
|
+
async readRecordedOwners() {
|
|
452
|
+
const owners = new Map();
|
|
453
|
+
let raw;
|
|
454
|
+
try {
|
|
455
|
+
raw = (await readRegularFileNoFollow(join(this.root, 'directory.yaml'))).toString('utf8');
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
if (!isNotFound(error)) {
|
|
459
|
+
this.integrityIssues.push('directory.yaml could not be read; Agent Home owners were not verified');
|
|
460
|
+
}
|
|
461
|
+
return owners;
|
|
462
|
+
}
|
|
463
|
+
const document = YAML.parse(raw);
|
|
464
|
+
// This store rewrites directory.yaml; never downgrade one written by a newer BizAgent.
|
|
465
|
+
assertKnownFormat(document, 1, 'directory.yaml');
|
|
466
|
+
const parsed = DIRECTORY_SCHEMA.safeParse(document);
|
|
467
|
+
if (!parsed.success) {
|
|
468
|
+
this.integrityIssues.push('directory.yaml is malformed; Agent Home owners were not verified');
|
|
469
|
+
return owners;
|
|
470
|
+
}
|
|
471
|
+
for (const entry of parsed.data.homes) {
|
|
472
|
+
if (entry.owner !== undefined)
|
|
473
|
+
owners.set(entry.address, entry.owner);
|
|
474
|
+
}
|
|
475
|
+
return owners;
|
|
476
|
+
}
|
|
477
|
+
/** Refuse an owner change made in agent-writable home.yaml; keep the recorded owner and report it. */
|
|
478
|
+
keepRecordedOwner(cache, recordedOwners) {
|
|
479
|
+
if (!recordedOwners.has(cache.home.address))
|
|
480
|
+
return;
|
|
481
|
+
const recorded = recordedOwners.get(cache.home.address) ?? undefined;
|
|
482
|
+
if (cache.home.owner === recorded)
|
|
483
|
+
return;
|
|
484
|
+
this.integrityIssues.push(`Agent Home ${cache.home.address}: home.yaml owner differs from the recorded owner; `
|
|
485
|
+
+ 'the change was refused and the recorded owner is kept');
|
|
486
|
+
const { owner: _ignored, ...rest } = cache.home;
|
|
487
|
+
cache.home = AgentHomeSchema.parse(recorded === undefined ? rest : { ...rest, owner: recorded });
|
|
488
|
+
}
|
|
489
|
+
async loadHome(directory) {
|
|
490
|
+
this.assertWithinRoot(directory);
|
|
491
|
+
const homeRaw = YAML.parse(await readManagedText(join(directory, 'home.yaml')));
|
|
492
|
+
assertKnownFormat(homeRaw, 1, 'home.yaml');
|
|
493
|
+
let home = AgentHomeSchema.parse(homeRaw);
|
|
494
|
+
// home.yaml lives in the Home's writable Workspace: its identity fields must match the address.
|
|
495
|
+
if (home.id !== safeHomeId(home.address)) {
|
|
496
|
+
throw new Error(`home.yaml id does not match its address ${home.address}`);
|
|
497
|
+
}
|
|
498
|
+
if (basename(directory) !== home.id) {
|
|
499
|
+
throw new Error(`Home directory name does not match home.yaml id ${home.id}`);
|
|
500
|
+
}
|
|
501
|
+
if (home.type !== homeTypeOf(home.address)) {
|
|
502
|
+
throw new Error(`home.yaml type ${home.type} does not match its address ${home.address}`);
|
|
503
|
+
}
|
|
504
|
+
const identity = normalizeBody(await readManagedText(join(directory, 'identity.md')));
|
|
505
|
+
const assets = new Map();
|
|
506
|
+
for (const [kind, child] of Object.entries(KIND_DIRECTORY)) {
|
|
507
|
+
const assetDirectory = join(directory, child);
|
|
508
|
+
await mkdir(assetDirectory, { recursive: true });
|
|
509
|
+
const entries = await readdir(assetDirectory, { withFileTypes: true });
|
|
510
|
+
for (const entry of entries) {
|
|
511
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
512
|
+
continue;
|
|
513
|
+
const bodyPath = join(assetDirectory, entry.name);
|
|
514
|
+
try {
|
|
515
|
+
const asset = parseAsset(await readManagedText(bodyPath), bodyPath);
|
|
516
|
+
if (asset.kind !== kind)
|
|
517
|
+
throw new Error(`${bodyPath}: kind ${asset.kind} does not match directory ${kind}`);
|
|
518
|
+
if (asset.ownerAddress !== home.address)
|
|
519
|
+
throw new Error(`${bodyPath}: owner does not match ${home.address}`);
|
|
520
|
+
if (assets.has(asset.id))
|
|
521
|
+
throw new Error(`${bodyPath}: duplicate asset id ${asset.id}`);
|
|
522
|
+
assets.set(asset.id, asset);
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
// One unreadable or invalid asset is left out instead of hiding the whole Home.
|
|
526
|
+
this.integrityIssues.push(`asset skipped: ${this.relativeMessage(error)}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const digest = contextDigest(identity, [...assets.values()]);
|
|
531
|
+
if (digest !== home.contextDigest) {
|
|
532
|
+
home = AgentHomeSchema.parse({
|
|
533
|
+
...home,
|
|
534
|
+
revision: home.revision + 1,
|
|
535
|
+
contextDigest: digest,
|
|
536
|
+
updatedAt: nowIso(),
|
|
537
|
+
});
|
|
538
|
+
await atomicWrite(join(directory, 'home.yaml'), YAML.stringify(home, { lineWidth: 0 }));
|
|
539
|
+
}
|
|
540
|
+
return { home, directory, identity, assets };
|
|
541
|
+
}
|
|
542
|
+
async loadOrganization() {
|
|
543
|
+
this.organizationLoadError = undefined;
|
|
544
|
+
try {
|
|
545
|
+
await this.loadOrganizationFile();
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
// The Homes stay usable; organization features wait until organization.yaml is fixed.
|
|
549
|
+
this.organization = undefined;
|
|
550
|
+
this.organizationLoadError = this.relativeMessage(error);
|
|
551
|
+
this.integrityIssues.push(`organization unavailable: ${this.organizationLoadError}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async loadOrganizationFile() {
|
|
555
|
+
let raw;
|
|
556
|
+
try {
|
|
557
|
+
raw = YAML.parse(await readManagedText(this.organizationFile));
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
if (isNotFound(error))
|
|
561
|
+
return;
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
assertKnownFormat(raw, 1, 'organization.yaml');
|
|
565
|
+
const organization = OrganizationSchema.parse(raw);
|
|
566
|
+
const referenced = new Set([
|
|
567
|
+
...(organization.lineHome === undefined ? [] : [organization.lineHome]),
|
|
568
|
+
organization.businessHome,
|
|
569
|
+
...organization.members.flatMap(member => [member.personalHome, member.roleHome]),
|
|
570
|
+
...organization.capabilityHomes,
|
|
571
|
+
...organization.links.flatMap(link => [link.from, link.to]),
|
|
572
|
+
]);
|
|
573
|
+
for (const address of referenced) {
|
|
574
|
+
if (!this.homes.has(address))
|
|
575
|
+
throw new Error(`organization references missing Agent Home: ${address}`);
|
|
576
|
+
}
|
|
577
|
+
this.organization = organization;
|
|
578
|
+
}
|
|
579
|
+
async createHomeUnlocked(input, writeDirectory, requireMatchingExisting) {
|
|
580
|
+
const address = parseHomeAddress(input.address);
|
|
581
|
+
const existing = this.homes.get(address);
|
|
582
|
+
if (existing !== undefined) {
|
|
583
|
+
if (requireMatchingExisting) {
|
|
584
|
+
const expectedName = input.displayName?.trim() || address;
|
|
585
|
+
const expectedOwner = input.owner?.trim();
|
|
586
|
+
const expectedIdentity = normalizeBody(input.identity ?? defaultIdentity(address));
|
|
587
|
+
if (existing.home.displayName !== expectedName
|
|
588
|
+
|| existing.home.owner !== expectedOwner
|
|
589
|
+
|| existing.identity !== expectedIdentity) {
|
|
590
|
+
throw new Error(`Agent Home already exists with a different definition: ${address}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return structuredClone(existing.home);
|
|
594
|
+
}
|
|
595
|
+
const id = safeHomeId(address);
|
|
596
|
+
const directory = join(this.homesRoot, id);
|
|
597
|
+
this.assertWithinRoot(directory);
|
|
598
|
+
if (await lstat(directory).then(() => true, () => false)) {
|
|
599
|
+
// A folder that did not load (damaged, or written by a newer BizAgent) is never replaced.
|
|
600
|
+
throw new Error(`a folder for Agent Home ${address} already exists but could not be loaded; `
|
|
601
|
+
+ 'run bizagent doctor for the reason');
|
|
602
|
+
}
|
|
603
|
+
const temporaryDirectory = `${directory}.${process.pid}.${randomUUID()}.tmp`;
|
|
604
|
+
this.assertWithinRoot(temporaryDirectory);
|
|
605
|
+
await mkdir(temporaryDirectory, { recursive: false });
|
|
606
|
+
let home;
|
|
607
|
+
let identity;
|
|
608
|
+
try {
|
|
609
|
+
for (const child of Object.values(KIND_DIRECTORY))
|
|
610
|
+
await mkdir(join(temporaryDirectory, child));
|
|
611
|
+
await mkdir(join(temporaryDirectory, 'worklogs'));
|
|
612
|
+
await mkdir(join(temporaryDirectory, 'archive'));
|
|
613
|
+
const createdAt = nowIso();
|
|
614
|
+
identity = normalizeBody(input.identity ?? defaultIdentity(address));
|
|
615
|
+
const digest = contextDigest(identity, []);
|
|
616
|
+
home = AgentHomeSchema.parse({
|
|
617
|
+
schemaVersion: 1,
|
|
618
|
+
id,
|
|
619
|
+
address,
|
|
620
|
+
type: homeTypeOf(address),
|
|
621
|
+
displayName: input.displayName?.trim() || address,
|
|
622
|
+
...(input.owner?.trim() ? { owner: input.owner.trim() } : {}),
|
|
623
|
+
revision: 1,
|
|
624
|
+
contextDigest: digest,
|
|
625
|
+
status: 'active',
|
|
626
|
+
createdAt,
|
|
627
|
+
updatedAt: createdAt,
|
|
628
|
+
});
|
|
629
|
+
await atomicWrite(join(temporaryDirectory, 'identity.md'), `${identity}\n`);
|
|
630
|
+
await atomicWrite(join(temporaryDirectory, 'home.yaml'), YAML.stringify(home, { lineWidth: 0 }));
|
|
631
|
+
await rename(temporaryDirectory, directory);
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
this.homes.set(address, { home, directory, identity, assets: new Map() });
|
|
638
|
+
if (writeDirectory)
|
|
639
|
+
await this.writeDirectory();
|
|
640
|
+
return structuredClone(home);
|
|
641
|
+
}
|
|
642
|
+
async refreshHome(cache) {
|
|
643
|
+
const digest = contextDigest(cache.identity, [...cache.assets.values()]);
|
|
644
|
+
if (digest === cache.home.contextDigest)
|
|
645
|
+
return;
|
|
646
|
+
cache.home = AgentHomeSchema.parse({
|
|
647
|
+
...cache.home,
|
|
648
|
+
revision: cache.home.revision + 1,
|
|
649
|
+
contextDigest: digest,
|
|
650
|
+
updatedAt: nowIso(),
|
|
651
|
+
});
|
|
652
|
+
await atomicWrite(join(cache.directory, 'home.yaml'), YAML.stringify(cache.home, { lineWidth: 0 }));
|
|
653
|
+
await this.writeDirectory();
|
|
654
|
+
}
|
|
655
|
+
assetPath(cache, kind, id) {
|
|
656
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(id))
|
|
657
|
+
throw new Error(`unsafe asset id: ${id}`);
|
|
658
|
+
const path = join(cache.directory, KIND_DIRECTORY[kind], `${id}.md`);
|
|
659
|
+
this.assertWithinRoot(path);
|
|
660
|
+
return path;
|
|
661
|
+
}
|
|
662
|
+
requireHome(address) {
|
|
663
|
+
const cache = this.homes.get(address);
|
|
664
|
+
if (cache === undefined)
|
|
665
|
+
throw new Error(`Agent Home not found: ${address}`);
|
|
666
|
+
if (cache.home.status !== 'active')
|
|
667
|
+
throw new Error(`Agent Home is not active: ${address}`);
|
|
668
|
+
return cache;
|
|
669
|
+
}
|
|
670
|
+
assertWithinRoot(path) {
|
|
671
|
+
const rel = relative(this.root, resolve(path));
|
|
672
|
+
if (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel)))
|
|
673
|
+
return;
|
|
674
|
+
throw new Error(`path escapes BizAgent root: ${path}`);
|
|
675
|
+
}
|
|
676
|
+
async writeDirectory() {
|
|
677
|
+
const directory = DIRECTORY_SCHEMA.parse({
|
|
678
|
+
schemaVersion: 1,
|
|
679
|
+
homes: [...this.homes.values()]
|
|
680
|
+
.map(cache => ({
|
|
681
|
+
id: cache.home.id,
|
|
682
|
+
address: cache.home.address,
|
|
683
|
+
path: relative(this.root, cache.directory),
|
|
684
|
+
owner: cache.home.owner ?? null,
|
|
685
|
+
}))
|
|
686
|
+
.sort((a, b) => a.address.localeCompare(b.address)),
|
|
687
|
+
});
|
|
688
|
+
await atomicWrite(join(this.root, 'directory.yaml'), YAML.stringify(directory, { lineWidth: 0 }));
|
|
689
|
+
}
|
|
690
|
+
mutate(operation) {
|
|
691
|
+
const next = this.mutationTail.then(operation, operation);
|
|
692
|
+
this.mutationTail = next.then(() => undefined, () => undefined);
|
|
693
|
+
return next;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function defaultIdentity(address) {
|
|
697
|
+
return `You are the long-term agent for ${address}. Learn from real work, keep evidence, and respect Home ownership.`;
|
|
698
|
+
}
|
|
699
|
+
function normalizeBody(body) {
|
|
700
|
+
const normalized = body.replace(/\r\n/g, '\n').trim();
|
|
701
|
+
if (normalized.length === 0)
|
|
702
|
+
throw new Error('body must not be empty');
|
|
703
|
+
return normalized;
|
|
704
|
+
}
|
|
705
|
+
function normalizeTags(tags) {
|
|
706
|
+
return [...new Set(tags.map(tag => tag.trim()).filter(Boolean))].sort();
|
|
707
|
+
}
|
|
708
|
+
function normalizeId(value, label) {
|
|
709
|
+
const normalized = value.trim().toLocaleLowerCase();
|
|
710
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(normalized)) {
|
|
711
|
+
throw new Error(`${label} id must use 1-64 lowercase letters, numbers, dots, underscores, or hyphens`);
|
|
712
|
+
}
|
|
713
|
+
return normalized;
|
|
714
|
+
}
|
|
715
|
+
function normalizeLabel(value, label) {
|
|
716
|
+
const normalized = value.trim();
|
|
717
|
+
if (normalized.length === 0)
|
|
718
|
+
throw new Error(`${label} must not be empty`);
|
|
719
|
+
if (normalized.length > 200)
|
|
720
|
+
throw new Error(`${label} must be at most 200 characters`);
|
|
721
|
+
return normalized;
|
|
722
|
+
}
|
|
723
|
+
/** Managed files are read without following a final symlink. */
|
|
724
|
+
async function readManagedText(path) {
|
|
725
|
+
return (await readRegularFileNoFollow(path)).toString('utf8');
|
|
726
|
+
}
|
|
727
|
+
function isNotFound(error) {
|
|
728
|
+
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
|
|
729
|
+
}
|
|
730
|
+
function stripStoredFields(asset) {
|
|
731
|
+
const { body: _body, bodyPath: _bodyPath, ...metadata } = asset;
|
|
732
|
+
return metadata;
|
|
733
|
+
}
|
|
734
|
+
function renderAsset(asset) {
|
|
735
|
+
const metadata = stripStoredFields(asset);
|
|
736
|
+
return `---\n${YAML.stringify(metadata, { lineWidth: 0 }).trimEnd()}\n---\n\n${asset.body}\n`;
|
|
737
|
+
}
|
|
738
|
+
function parseAsset(raw, bodyPath) {
|
|
739
|
+
// A git checkout with autocrlf turns frontmatter delimiters into CRLF; parse those the same way.
|
|
740
|
+
const input = raw.replace(/\r\n/g, '\n');
|
|
741
|
+
if (!input.startsWith('---\n'))
|
|
742
|
+
throw new Error(`${bodyPath}: missing YAML frontmatter`);
|
|
743
|
+
const end = input.indexOf('\n---\n', 4);
|
|
744
|
+
if (end < 0)
|
|
745
|
+
throw new Error(`${bodyPath}: unterminated YAML frontmatter`);
|
|
746
|
+
const frontmatter = YAML.parse(input.slice(4, end));
|
|
747
|
+
assertKnownFormat(frontmatter, 1, bodyPath);
|
|
748
|
+
const metadata = AssetMetadataSchema.parse(frontmatter);
|
|
749
|
+
const body = normalizeBody(input.slice(end + 5));
|
|
750
|
+
return { ...metadata, body, bodyPath };
|
|
751
|
+
}
|
|
752
|
+
function contextDigest(identity, assets) {
|
|
753
|
+
const activeIndex = assets
|
|
754
|
+
.filter(asset => asset.status === 'active')
|
|
755
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
756
|
+
.map(asset => ({
|
|
757
|
+
id: asset.id,
|
|
758
|
+
kind: asset.kind,
|
|
759
|
+
description: asset.description,
|
|
760
|
+
tags: asset.tags,
|
|
761
|
+
revision: asset.revision,
|
|
762
|
+
bodyDigest: createHash('sha256').update(asset.body).digest('hex'),
|
|
763
|
+
}));
|
|
764
|
+
return createHash('sha256')
|
|
765
|
+
.update(JSON.stringify({ identity, activeIndex }))
|
|
766
|
+
.digest('hex');
|
|
767
|
+
}
|
|
768
|
+
function compareAssets(a, b) {
|
|
769
|
+
const status = statusRank(a.status) - statusRank(b.status);
|
|
770
|
+
if (status !== 0)
|
|
771
|
+
return status;
|
|
772
|
+
const kind = kindRank(a.kind) - kindRank(b.kind);
|
|
773
|
+
if (kind !== 0)
|
|
774
|
+
return kind;
|
|
775
|
+
if (a.fitness !== b.fitness)
|
|
776
|
+
return b.fitness - a.fitness;
|
|
777
|
+
if (a.updatedAt !== b.updatedAt)
|
|
778
|
+
return b.updatedAt.localeCompare(a.updatedAt);
|
|
779
|
+
return a.id.localeCompare(b.id);
|
|
780
|
+
}
|
|
781
|
+
function statusRank(status) {
|
|
782
|
+
return { active: 0, candidate: 1, superseded: 2, retired: 3 }[status];
|
|
783
|
+
}
|
|
784
|
+
function kindRank(kind) {
|
|
785
|
+
return { identity: 0, knowledge: 1, method: 2, insight: 3, memory: 4 }[kind];
|
|
786
|
+
}
|
|
787
|
+
function indexRow(asset) {
|
|
788
|
+
const tags = asset.tags.length === 0 ? '' : ` tags=${asset.tags.map(escapeIndexText).join(',')}`;
|
|
789
|
+
return `- [${escapeIndexText(asset.id)}] ${asset.kind}: ${escapeIndexText(asset.description)}${tags}`;
|
|
790
|
+
}
|
|
791
|
+
function escapeIndexText(value) {
|
|
792
|
+
return value.replace(/[<>\r\n]/g, character => ({ '<': '<', '>': '>', '\r': ' ', '\n': ' ' })[character] ?? ' ');
|
|
793
|
+
}
|
|
794
|
+
function takeWithinBudget(rows, maxBytes) {
|
|
795
|
+
const included = [];
|
|
796
|
+
let used = 0;
|
|
797
|
+
for (const row of rows) {
|
|
798
|
+
const bytes = Buffer.byteLength(row) + (included.length === 0 ? 0 : 1);
|
|
799
|
+
if (used + bytes > maxBytes)
|
|
800
|
+
continue;
|
|
801
|
+
included.push(row);
|
|
802
|
+
used += bytes;
|
|
803
|
+
}
|
|
804
|
+
return { included, omitted: rows.length - included.length };
|
|
805
|
+
}
|
|
806
|
+
function truncateUtf8(value, maxBytes) {
|
|
807
|
+
if (Buffer.byteLength(value) <= maxBytes)
|
|
808
|
+
return value;
|
|
809
|
+
let result = '';
|
|
810
|
+
let bytes = 0;
|
|
811
|
+
for (const character of value) {
|
|
812
|
+
const next = Buffer.byteLength(character);
|
|
813
|
+
if (bytes + next > Math.max(maxBytes - 32, 0))
|
|
814
|
+
break;
|
|
815
|
+
result += character;
|
|
816
|
+
bytes += next;
|
|
817
|
+
}
|
|
818
|
+
return `${result}\n\n[Identity truncated by byte budget.]`;
|
|
819
|
+
}
|
|
820
|
+
async function atomicWrite(path, contents) {
|
|
821
|
+
await writeFileDurably(path, contents);
|
|
822
|
+
}
|
|
823
|
+
function errorMessage(error) {
|
|
824
|
+
return error instanceof Error ? error.message : String(error);
|
|
825
|
+
}
|
|
826
|
+
export async function pathExists(path) {
|
|
827
|
+
try {
|
|
828
|
+
await stat(path);
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
|
|
833
|
+
return false;
|
|
834
|
+
throw error;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
//# sourceMappingURL=home-store.js.map
|