@xingular/harness 2.1.2 → 3.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 +12 -0
- package/NOTICE.md +22 -0
- package/README.md +67 -36
- package/dist/cli.js +6023 -6621
- package/dist/terminal-pi.js +19 -24
- package/docs/terminal-tui.md +27 -7
- package/package.json +2 -2
- package/src/accounts/accounts.ts +4 -3
- package/src/capabilities/api.ts +1 -1
- package/src/cli.ts +7 -3
- package/src/core/caller.ts +34 -0
- package/src/core/fs.ts +17 -1
- package/src/harness.ts +55 -66
- package/src/{memory → history}/recall.test.ts +1 -1
- package/src/{memory → history}/recall.ts +1 -1
- package/src/{memory → history}/trajectory.test.ts +4 -4
- package/src/{memory → history}/trajectory.ts +1 -1
- package/src/hub/hub.test.ts +4 -5
- package/src/hub/hub.ts +2 -2
- package/src/index.ts +28 -14
- package/src/mcp/connections.ts +1 -1
- package/src/mcp/service.ts +1 -1
- package/src/pi/proxy.test.ts +68 -0
- package/src/pi/runtime.test.ts +61 -0
- package/src/pi/runtime.ts +50 -0
- package/src/plugins/api.ts +11 -9
- package/src/plugins/builtin.ts +22 -40
- package/src/plugins/catalogue.test.ts +13 -41
- package/src/plugins/catalogue.ts +3 -32
- package/src/plugins/discover.ts +2 -1
- package/src/plugins/firstParty.test.ts +1 -1
- package/src/plugins/hooks.test.ts +21 -71
- package/src/plugins/hooks.ts +6 -7
- package/src/plugins/identity.ts +16 -2
- package/src/plugins/install.test.ts +1 -1
- package/src/plugins/install.ts +2 -1
- package/src/plugins/inventory.ts +19 -18
- package/src/plugins/plugins.ts +38 -2
- package/src/prompt/compile.test.ts +82 -193
- package/src/prompt/compile.ts +98 -363
- package/src/refine/cli.ts +114 -0
- package/src/refine/demo.ts +124 -0
- package/src/refine/evidence.ts +349 -0
- package/src/refine/host.ts +102 -0
- package/src/refine/ledger.ts +104 -0
- package/src/refine/library.ts +192 -0
- package/src/refine/pi.test.ts +90 -0
- package/src/refine/pi.ts +218 -0
- package/src/refine/prompts.ts +430 -0
- package/src/refine/recursion.ts +353 -0
- package/src/refine/refine.test.ts +140 -0
- package/src/refine/runner.ts +205 -0
- package/src/refine/selection.ts +291 -0
- package/src/refine/service.ts +194 -0
- package/src/refine/trajectory.ts +71 -0
- package/src/refine/types.ts +91 -0
- package/src/server/methods.ts +137 -123
- package/src/server/server.test.ts +37 -30
- package/src/sessions/contracts.ts +6 -1
- package/src/sessions/conversation.test.ts +2 -27
- package/src/sessions/policy.ts +1 -1
- package/src/sessions/pricing.ts +3 -1
- package/src/sessions/registry.ts +5 -1
- package/src/sessions/runStore.ts +3 -1
- package/src/sessions/runner.ts +10 -5
- package/src/sessions/sessions.test.ts +45 -6
- package/src/sessions/sessions.ts +40 -17
- package/src/sessions/subagentService.test.ts +22 -1
- package/src/sessions/subagentService.ts +14 -1
- package/src/sessions/subagents.ts +2 -0
- package/src/terminal/cli.ts +24 -48
- package/src/terminal/pluginControl.test.ts +7 -8
- package/src/terminal/socket.ts +3 -3
- package/src/terminal/terminal.test.ts +21 -39
- package/src/terminal/terminals.ts +20 -13
- package/src/terminal/tui/application.test.ts +58 -0
- package/src/terminal/tui/application.ts +10 -4
- package/src/terminal/tui/clipboardFiles.test.ts +94 -0
- package/src/terminal/tui/clipboardFiles.ts +86 -0
- package/src/terminal/tui/piAdapter.ts +7 -4
- package/src/terminal/tui/runtime.ts +2 -0
- package/src/tools/definitions.ts +49 -132
- package/src/tools/dispatch.ts +78 -0
- package/src/tools/harnessTools.ts +23 -0
- package/src/tools/spawnSubagent.ts +3 -3
- package/src/tools/types.ts +1 -1
- package/src/tree/components.ts +221 -0
- package/src/tree/materialise.ts +122 -0
- package/src/tree/seed.ts +123 -0
- package/src/tree/store.ts +193 -0
- package/src/tree/surface.ts +135 -0
- package/src/tree/tree.test.ts +126 -0
- package/src/tree/tree.ts +346 -0
- package/src/tree/trust.ts +86 -0
- package/src/wiki/notes.ts +313 -0
- package/src/memory/api.test.ts +0 -82
- package/src/memory/api.ts +0 -303
- package/src/memory/blocks.test.ts +0 -78
- package/src/memory/blocks.ts +0 -408
- package/src/memory/caller.ts +0 -69
- package/src/memory/curation.test.ts +0 -206
- package/src/memory/curation.ts +0 -64
- package/src/memory/dispatch.ts +0 -64
- package/src/memory/facts.test.ts +0 -41
- package/src/memory/facts.ts +0 -248
- package/src/memory/graph.ts +0 -103
- package/src/memory/health.test.ts +0 -130
- package/src/memory/health.ts +0 -74
- package/src/memory/maintainer.ts +0 -272
- package/src/memory/maintenance.test.ts +0 -192
- package/src/memory/maintenance.ts +0 -679
- package/src/memory/migrate.ts +0 -79
- package/src/memory/operations.ts +0 -150
- package/src/memory/proposals.test.ts +0 -168
- package/src/memory/proposals.ts +0 -584
- package/src/memory/references.ts +0 -77
- package/src/memory/runContext.ts +0 -93
- package/src/memory/store.test.ts +0 -66
- package/src/memory/store.ts +0 -385
- package/src/memory/wiki.test.ts +0 -47
- package/src/memory/wiki.ts +0 -423
- package/src/memory/wikiIndex.test.ts +0 -88
- package/src/memory/wikiIndex.ts +0 -117
- package/src/plugins/proposal.ts +0 -121
- package/src/skills/registry.test.ts +0 -104
- package/src/skills/registry.ts +0 -413
- package/src/tools/maintenance.ts +0 -29
- package/src/tools/memoryTools.ts +0 -22
- package/src/tools/pluginPropose.ts +0 -41
- package/src/tools/registryWrite.test.ts +0 -101
- package/src/tools/registryWrite.ts +0 -182
- /package/src/{memory → core}/secrets.ts +0 -0
package/src/memory/blocks.ts
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
// Core memory: blocks. A block is a short Markdown file that is always in the context of what
|
|
2
|
-
// it is attached to, and that the model edits itself. Blocks live under `wiki/blocks/`:
|
|
3
|
-
// `shared/<label>.md` (the `user` block and custom shared blocks), `agents/<agent-id>/` (an
|
|
4
|
-
// agent's `persona` and `notes`), and `projects/<slug>/brief.md`. A block's id and scope come
|
|
5
|
-
// from where its file lives. Every edit is a revision and an undoable operation.
|
|
6
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
7
|
-
import { join, relative } from 'node:path'
|
|
8
|
-
|
|
9
|
-
import { noteChange } from '../core/changes'
|
|
10
|
-
import { HarnessError, invalid, notFound } from '../core/errors'
|
|
11
|
-
import * as frontmatter from '../core/frontmatter'
|
|
12
|
-
import { atomicWrite, newId, now, safeId, writeJson } from '../core/fs'
|
|
13
|
-
import { listProjects, projectsOrNone } from '../core/projects'
|
|
14
|
-
import { charCount, comparePaths, compareText } from '../core/text'
|
|
15
|
-
import { listOperations, undo, type Operation } from './operations'
|
|
16
|
-
import { looksSecret } from './secrets'
|
|
17
|
-
import * as store from './store'
|
|
18
|
-
|
|
19
|
-
export const DEFAULT_LIMIT = 2000
|
|
20
|
-
export const PROJECT_LIMIT = 3000
|
|
21
|
-
export const MAX_LIMIT = 20_000
|
|
22
|
-
/** An agent's prompt carries at most this many project briefs; the rest are listed by name. */
|
|
23
|
-
export const AGENT_PROJECT_BRIEFS = 2
|
|
24
|
-
const AGENT_LABELS = ['persona', 'notes']
|
|
25
|
-
|
|
26
|
-
export interface BlockFront {
|
|
27
|
-
label: string
|
|
28
|
-
/** Tells the model what belongs in the block. */
|
|
29
|
-
description: string
|
|
30
|
-
limit: number
|
|
31
|
-
/** Only the user may edit it. */
|
|
32
|
-
readOnly: boolean
|
|
33
|
-
/** Custom shared blocks: the agents it is attached to; empty means every agent. */
|
|
34
|
-
agents: string[]
|
|
35
|
-
updatedAt: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type BlockScope = 'shared' | 'agent' | 'project'
|
|
39
|
-
|
|
40
|
-
export interface Block {
|
|
41
|
-
/** `blocks/shared/user`, `blocks/agents/<agent-id>/persona`, `blocks/projects/<slug>/brief`. */
|
|
42
|
-
id: string
|
|
43
|
-
scope: BlockScope
|
|
44
|
-
/** The agent id or project slug the block belongs to. */
|
|
45
|
-
owner: string | null
|
|
46
|
-
front: BlockFront
|
|
47
|
-
body: string
|
|
48
|
-
path: string
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Who is reading or editing blocks. */
|
|
52
|
-
export type Caller =
|
|
53
|
-
| { kind: 'user' }
|
|
54
|
-
/** An agent with blocks of its own, and the name recorded on its revisions. */
|
|
55
|
-
| { kind: 'agent'; id: string; name: string; vaultAccess?: 'self' | 'propose' | 'curator' }
|
|
56
|
-
/** A session without blocks of its own, and its project, if any. */
|
|
57
|
-
| { kind: 'session'; projectSlug: string | null }
|
|
58
|
-
|
|
59
|
-
function actorOf(caller: Caller): string {
|
|
60
|
-
return caller.kind === 'user' ? 'user' : caller.kind === 'agent' ? caller.name : 'Harness session'
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function blocksDir(wiki: string): string {
|
|
64
|
-
return join(wiki, 'blocks')
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function locate(wiki: string, path: string): { id: string; scope: BlockScope; owner: string | null } | null {
|
|
68
|
-
const inside = relative(blocksDir(wiki), path)
|
|
69
|
-
if (inside.startsWith('..')) return null
|
|
70
|
-
const parts = inside.replace(/(?<=[^/])\.[^./]*$/, '').split('/')
|
|
71
|
-
const id = `blocks/${parts.join('/')}`
|
|
72
|
-
if (parts.length === 2 && parts[0] === 'shared') return { id, scope: 'shared', owner: null }
|
|
73
|
-
if (parts.length === 3 && parts[0] === 'agents') return { id, scope: 'agent', owner: parts[1] }
|
|
74
|
-
if (parts.length === 3 && parts[0] === 'projects' && parts[2] === 'brief') return { id, scope: 'project', owner: parts[1] }
|
|
75
|
-
return null
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function parseFront(front: Record<string, unknown>): BlockFront | null {
|
|
79
|
-
const { label, description, limit, readOnly, updatedAt } = front
|
|
80
|
-
if (typeof label !== 'string') return null
|
|
81
|
-
if (description !== undefined && typeof description !== 'string') return null
|
|
82
|
-
if (limit !== undefined && !(typeof limit === 'number' && Number.isInteger(limit) && limit >= 0)) return null
|
|
83
|
-
if (readOnly !== undefined && typeof readOnly !== 'boolean') return null
|
|
84
|
-
if (updatedAt !== undefined && typeof updatedAt !== 'string') return null
|
|
85
|
-
const agents = store.stringList(front.agents)
|
|
86
|
-
if (!agents) return null
|
|
87
|
-
return { label, description: description ?? '', limit: limit ?? DEFAULT_LIMIT, readOnly: readOnly ?? false, agents, updatedAt: updatedAt ?? '' }
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function serializeBlock(front: BlockFront, body: string): string {
|
|
91
|
-
return frontmatter.serialize(
|
|
92
|
-
{ label: front.label, description: front.description, limit: front.limit, readOnly: front.readOnly, ...(front.agents.length ? { agents: front.agents } : {}), updatedAt: front.updatedAt },
|
|
93
|
-
body
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function parse(wiki: string, path: string): Block | null {
|
|
98
|
-
const located = locate(wiki, path)
|
|
99
|
-
if (!located) return null
|
|
100
|
-
let text: string
|
|
101
|
-
try {
|
|
102
|
-
text = readFileSync(path, 'utf8')
|
|
103
|
-
} catch {
|
|
104
|
-
return null
|
|
105
|
-
}
|
|
106
|
-
const parsed = frontmatter.parse(text)
|
|
107
|
-
const front = parsed && parseFront(parsed.front)
|
|
108
|
-
if (!parsed || !front) return null
|
|
109
|
-
return { ...located, front, body: parsed.body.trimEnd(), path }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function loadAll(wiki: string): Block[] {
|
|
113
|
-
const files: string[] = []
|
|
114
|
-
store.markdownFiles(blocksDir(wiki), files)
|
|
115
|
-
return files.sort(comparePaths).flatMap((path) => parse(wiki, path) ?? [])
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** A block by its id (`blocks/shared/user`). Every segment must be a safe identifier. */
|
|
119
|
-
export function find(wiki: string, id: string): Block {
|
|
120
|
-
if (!id.startsWith('blocks/')) throw notFound('Memory block')
|
|
121
|
-
const segments = id.slice('blocks/'.length).split('/')
|
|
122
|
-
try {
|
|
123
|
-
segments.forEach(safeId)
|
|
124
|
-
} catch {
|
|
125
|
-
throw notFound('Memory block')
|
|
126
|
-
}
|
|
127
|
-
const block = parse(wiki, `${join(blocksDir(wiki), ...segments)}.md`)
|
|
128
|
-
if (!block) throw notFound('Memory block')
|
|
129
|
-
return block
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function create(wiki: string, path: string, label: string, description: string, limit: number): Block {
|
|
133
|
-
const file = join(blocksDir(wiki), `${path}.md`)
|
|
134
|
-
if (!existsSync(file)) atomicWrite(file, serializeBlock({ label, description, limit, readOnly: false, agents: [], updatedAt: now() }, ''))
|
|
135
|
-
const block = parse(wiki, file)
|
|
136
|
-
if (!block) throw new HarnessError('invalid_data', `The ${label} memory block could not be read.`)
|
|
137
|
-
return block
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function ensureUser(wiki: string): Block {
|
|
141
|
-
return create(wiki, 'shared/user', 'user', 'What you know about the user: name, role, preferences, and how they like to work. Shared with every agent and session.', DEFAULT_LIMIT)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function ensureAgent(wiki: string, agentId: string): Block[] {
|
|
145
|
-
const agent = safeId(agentId)
|
|
146
|
-
return [
|
|
147
|
-
create(wiki, `agents/${agent}/persona`, 'persona', 'How you work: your role for this user, your habits, and what you have learned about doing this job well.', DEFAULT_LIMIT),
|
|
148
|
-
create(wiki, `agents/${agent}/notes`, 'notes', 'Working notes to keep across turns: what you are in the middle of and what to follow up. Prune what is done.', DEFAULT_LIMIT)
|
|
149
|
-
]
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function ensureProject(wiki: string, slug: string, name: string): Block {
|
|
153
|
-
const safe = safeId(slug)
|
|
154
|
-
return create(
|
|
155
|
-
wiki,
|
|
156
|
-
`projects/${safe}/brief`,
|
|
157
|
-
`project/${safe}`,
|
|
158
|
-
`What ${name.replaceAll('\n', ' ')} is, its stack and conventions, and its current state. Shared by everyone working in this project.`,
|
|
159
|
-
PROJECT_LIMIT
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* The blocks in a caller's context, in the order they are shown. Missing default blocks are
|
|
165
|
-
* created. `projectSlugs` are the projects whose briefs an agent carries (most recent first).
|
|
166
|
-
*/
|
|
167
|
-
export function attached(root: string, caller: Caller, projectSlugs: readonly string[]): Block[] {
|
|
168
|
-
const wiki = store.wikiDir(root)
|
|
169
|
-
const projectName = (slug: string): string | null => projectsOrNone(root).find((project) => project.slug === slug)?.name ?? null
|
|
170
|
-
if (caller.kind === 'user') return loadAll(wiki)
|
|
171
|
-
const blocks: Block[] = []
|
|
172
|
-
if (caller.kind === 'agent') {
|
|
173
|
-
blocks.push(...ensureAgent(wiki, caller.id))
|
|
174
|
-
blocks.push(ensureUser(wiki))
|
|
175
|
-
blocks.push(...loadAll(wiki).filter((block) => block.scope === 'shared' && block.front.label !== 'user' && (!block.front.agents.length || block.front.agents.includes(caller.id))))
|
|
176
|
-
// The most recently active projects are chosen, then listed in a fixed order: later activity
|
|
177
|
-
// must not reshuffle the context a prompt cache was built on.
|
|
178
|
-
for (const slug of projectSlugs.slice(0, AGENT_PROJECT_BRIEFS).sort(compareText)) {
|
|
179
|
-
const name = projectName(slug)
|
|
180
|
-
if (name !== null) blocks.push(ensureProject(wiki, slug, name))
|
|
181
|
-
}
|
|
182
|
-
} else {
|
|
183
|
-
blocks.push(ensureUser(wiki))
|
|
184
|
-
const name = caller.projectSlug ? projectName(caller.projectSlug) : null
|
|
185
|
-
if (caller.projectSlug && name !== null) blocks.push(ensureProject(wiki, caller.projectSlug, name))
|
|
186
|
-
}
|
|
187
|
-
return blocks
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Why `caller` may not change `block`, or `null` when it may; the text is what the model sees. */
|
|
191
|
-
export function editRefusal(caller: Caller, block: Block): string | null {
|
|
192
|
-
const label = block.front.label
|
|
193
|
-
if (caller.kind === 'user') return null
|
|
194
|
-
if (block.front.readOnly) return `The ${label} block is read-only; only the user can change it.`
|
|
195
|
-
if (caller.kind === 'agent' && caller.vaultAccess === 'propose') return 'Memory changes are proposals in this session. Submit the block change with wiki_propose; the next memory merge applies it.'
|
|
196
|
-
if (caller.kind === 'agent' && caller.vaultAccess === 'self' && (block.scope !== 'agent' || block.owner !== caller.id || !['persona', 'notes'].includes(label))) return 'This field is outside your writable memory. Submit a block change with wiki_propose for the curator to review.'
|
|
197
|
-
if (caller.kind === 'agent') {
|
|
198
|
-
if (block.scope === 'agent') return block.owner === caller.id ? null : 'That block belongs to another agent.'
|
|
199
|
-
if (block.scope === 'shared') return !block.front.agents.length || block.front.agents.includes(caller.id) ? null : `The ${label} block is not attached to you.`
|
|
200
|
-
return null
|
|
201
|
-
}
|
|
202
|
-
if (block.scope === 'project') return block.owner === caller.projectSlug ? null : "A session may edit only its own project's brief."
|
|
203
|
-
return `A Harness session can read the ${label} block but cannot edit it. Report what you learned in your result instead.`
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Finds the block a caller means by `label`: `user`, `persona`, `notes`, `project/<slug>`
|
|
208
|
-
* (or `project` for a session's own), a custom shared label, or a full block id.
|
|
209
|
-
*/
|
|
210
|
-
export function resolve(root: string, caller: Caller, rawLabel: string): Block {
|
|
211
|
-
const wiki = store.wikiDir(root)
|
|
212
|
-
const label = rawLabel.trim().replace(/^\/+/, '')
|
|
213
|
-
const unknown = () => invalid(`There is no memory block called ${label}. Use one of the labels shown in your memory blocks.`)
|
|
214
|
-
const orUnknown = (id: string): Block => {
|
|
215
|
-
try {
|
|
216
|
-
return find(wiki, id)
|
|
217
|
-
} catch {
|
|
218
|
-
throw unknown()
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (label.startsWith('blocks/')) return orUnknown(label)
|
|
222
|
-
if (AGENT_LABELS.includes(label)) {
|
|
223
|
-
if (caller.kind !== 'agent') throw invalid(`The ${label} block belongs to an agent, not to a session.`)
|
|
224
|
-
return ensureAgent(wiki, caller.id).find((block) => block.front.label === label)!
|
|
225
|
-
}
|
|
226
|
-
if (label === 'project' || label.startsWith('project/')) {
|
|
227
|
-
const slug = label.startsWith('project/') ? label.slice('project/'.length) : caller.kind === 'session' ? caller.projectSlug : null
|
|
228
|
-
if (slug === null) throw invalid('Name the project: project/<slug>.')
|
|
229
|
-
const project = listProjects(root).find((entry) => entry.slug === slug)
|
|
230
|
-
if (!project) throw unknown()
|
|
231
|
-
return ensureProject(wiki, project.slug, project.name)
|
|
232
|
-
}
|
|
233
|
-
if (label === 'user') return ensureUser(wiki)
|
|
234
|
-
return orUnknown(`blocks/shared/${label}`)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export type Edit = { kind: 'append'; text: string } | { kind: 'replace'; old: string; new: string } | { kind: 'rethink'; text: string }
|
|
238
|
-
|
|
239
|
-
/** The body after an edit. Pure, so the rules are testable. Throws the reason an edit is refused. */
|
|
240
|
-
export function applyEdit(body: string, edit: Edit): string {
|
|
241
|
-
if (edit.kind === 'append') {
|
|
242
|
-
const text = edit.text.trim()
|
|
243
|
-
if (!text) throw invalid('Nothing to append.')
|
|
244
|
-
return body.trim() ? `${body.trimEnd()}\n${text}` : text
|
|
245
|
-
}
|
|
246
|
-
if (edit.kind === 'replace') {
|
|
247
|
-
if (!edit.old) throw invalid('Give the exact passage to replace.')
|
|
248
|
-
const count = body.split(edit.old).length - 1
|
|
249
|
-
if (count === 0) throw invalid('That passage is not in the block. Copy it exactly as it appears.')
|
|
250
|
-
if (count > 1) throw invalid(`That passage appears ${count} times. Include more of the surrounding text so it is unique.`)
|
|
251
|
-
return body.replace(edit.old, () => edit.new).trim()
|
|
252
|
-
}
|
|
253
|
-
return edit.text.trim()
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Why `body` cannot be stored in the block, or `null`. */
|
|
257
|
-
export function check(front: BlockFront, body: string): string | null {
|
|
258
|
-
const length = charCount(body)
|
|
259
|
-
if (length > front.limit) {
|
|
260
|
-
return `The ${front.label} block would be ${length} of ${front.limit} characters. Condense it with memory_rethink, or move detail to the Wiki with wiki_propose.`
|
|
261
|
-
}
|
|
262
|
-
if (looksSecret(body) || looksSecret(front.description)) return 'That text looks like it contains a credential, so it was not stored.'
|
|
263
|
-
return null
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** Writes a block's new content with a revision before and after and an undo operation. */
|
|
267
|
-
function write(wiki: string, block: Block, front: BlockFront, body: string, actor: string, reason: string): Block {
|
|
268
|
-
let current = ''
|
|
269
|
-
try {
|
|
270
|
-
current = readFileSync(block.path, 'utf8')
|
|
271
|
-
} catch {
|
|
272
|
-
current = ''
|
|
273
|
-
}
|
|
274
|
-
const next = serializeBlock(front, body)
|
|
275
|
-
if (next === current) return block
|
|
276
|
-
// The undo target: the latest revision when it already holds the current text, else a new one.
|
|
277
|
-
const latest = store.currentRevision(wiki, block.id)
|
|
278
|
-
let before: string | null = null
|
|
279
|
-
if (latest) {
|
|
280
|
-
try {
|
|
281
|
-
if (store.revisionText(wiki, block.id, latest) === current) before = latest
|
|
282
|
-
} catch {
|
|
283
|
-
before = null
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
before ??= store.snapshot(wiki, block.id, current, actor, 'Before edit').id
|
|
287
|
-
atomicWrite(block.path, next)
|
|
288
|
-
const after = store.snapshot(wiki, block.id, next, actor, reason)
|
|
289
|
-
noteChange({ wiki, id: block.id, path: block.path, text: next, actor, reason })
|
|
290
|
-
const operation: Operation = {
|
|
291
|
-
id: newId(),
|
|
292
|
-
createdAt: now(),
|
|
293
|
-
trigger: `${actor}: ${reason} (${front.label})`,
|
|
294
|
-
pages: [{ pageId: block.id, path: block.path, beforeRevisionId: before, afterRevisionId: after.id }],
|
|
295
|
-
undone: false
|
|
296
|
-
}
|
|
297
|
-
writeJson(join(wiki, 'operations', `${operation.id}.json`), operation)
|
|
298
|
-
store.log(wiki, `- ${now()} · ${actor} · block · \`${block.id}\` ${reason} · revision \`${after.id}\``)
|
|
299
|
-
return find(wiki, block.id)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** A model's edit through `memory_append`, `memory_replace`, or `memory_rethink`. */
|
|
303
|
-
export function edit(root: string, caller: Caller, label: string, change: Edit): Block {
|
|
304
|
-
const wiki = store.wikiDir(root)
|
|
305
|
-
const block = resolve(root, caller, label)
|
|
306
|
-
const refusal = editRefusal(caller, block)
|
|
307
|
-
if (refusal) throw invalid(refusal)
|
|
308
|
-
const body = applyEdit(block.body, change)
|
|
309
|
-
const problem = check(block.front, body)
|
|
310
|
-
if (problem) throw invalid(problem)
|
|
311
|
-
const reason = change.kind === 'append' ? 'appended' : change.kind === 'replace' ? 'replaced a passage' : 'rewrote'
|
|
312
|
-
return write(wiki, block, { ...block.front, updatedAt: now() }, body, actorOf(caller), reason)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export interface BlockInput {
|
|
316
|
-
/** An existing block id, or none to create a shared block from `label`. */
|
|
317
|
-
id?: string | null
|
|
318
|
-
label?: string | null
|
|
319
|
-
body: string
|
|
320
|
-
description: string
|
|
321
|
-
limit: number
|
|
322
|
-
readOnly?: boolean
|
|
323
|
-
agents?: string[]
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/** The user's edit in the Wiki, or a new custom shared block. */
|
|
327
|
-
export function saveAsUser(wiki: string, input: BlockInput): Block {
|
|
328
|
-
if (!Number.isInteger(input.limit) || input.limit <= 0 || input.limit > MAX_LIMIT) throw invalid(`A block's limit is between 1 and ${MAX_LIMIT} characters.`)
|
|
329
|
-
let block: Block
|
|
330
|
-
if (input.id) {
|
|
331
|
-
block = find(wiki, input.id)
|
|
332
|
-
} else {
|
|
333
|
-
const label = store.slug(input.label ?? '')
|
|
334
|
-
if (!(input.label ?? '').trim() || AGENT_LABELS.includes(label) || label === 'user' || label.startsWith('project')) {
|
|
335
|
-
throw invalid('Give the block a label that is not user, persona, notes, or project.')
|
|
336
|
-
}
|
|
337
|
-
let exists = true
|
|
338
|
-
try {
|
|
339
|
-
find(wiki, `blocks/shared/${label}`)
|
|
340
|
-
} catch {
|
|
341
|
-
exists = false
|
|
342
|
-
}
|
|
343
|
-
if (exists) throw invalid(`A shared block called ${label} already exists.`)
|
|
344
|
-
block = create(wiki, `shared/${label}`, label, input.description.trim(), input.limit)
|
|
345
|
-
}
|
|
346
|
-
const front: BlockFront = { ...block.front, description: input.description.trim(), limit: input.limit, readOnly: input.readOnly ?? false, updatedAt: now() }
|
|
347
|
-
if (block.scope === 'shared' && front.label !== 'user') front.agents = input.agents ?? []
|
|
348
|
-
const body = input.body.trim()
|
|
349
|
-
const problem = check(front, body)
|
|
350
|
-
if (problem) throw invalid(problem)
|
|
351
|
-
return write(wiki, block, front, body, 'user', input.id ? 'edited' : 'created')
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/** The most recent edit of a block that has not been undone yet. */
|
|
355
|
-
function lastEdit(wiki: string, blockId: string): Operation | null {
|
|
356
|
-
return (
|
|
357
|
-
listOperations(wiki)
|
|
358
|
-
.filter((operation) => !operation.undone && operation.pages.length === 1 && operation.pages[0].pageId === blockId)
|
|
359
|
-
.sort((left, right) => compareText(left.createdAt, right.createdAt))
|
|
360
|
-
.at(-1) ?? null
|
|
361
|
-
)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export function undoLast(wiki: string, blockId: string): Block {
|
|
365
|
-
const latest = lastEdit(wiki, blockId)
|
|
366
|
-
if (!latest) throw invalid('There is no edit to undo.')
|
|
367
|
-
undo(wiki, latest.id)
|
|
368
|
-
return find(wiki, blockId)
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* The memory-blocks section of a compiled prompt. `cli` names the `xagent-wiki` commands in
|
|
373
|
-
* place of the tools, for sessions that have the command and not the tools.
|
|
374
|
-
*/
|
|
375
|
-
export function render(blocks: readonly Block[], caller: Caller, cli: boolean): string {
|
|
376
|
-
if (!blocks.length) return ''
|
|
377
|
-
const how = cli
|
|
378
|
-
? '`xagent-wiki block append <label> <text>`, `block replace <label> --old <text> --new <text>`, or `block rethink <label> <text>`'
|
|
379
|
-
: 'memory_append, memory_replace, or memory_rethink'
|
|
380
|
-
return `## Memory blocks\n\nThese blocks are your memory that is always in view, and they persist across conversations. When you learn something worth keeping, edit a block you may change with ${how}. Keep each block within its limit.\n${renderItems(blocks, caller)}`
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/** The blocks alone, each framed with its label, how full it is, and what belongs in it. */
|
|
384
|
-
export function renderItems(blocks: readonly Block[], caller: Caller): string {
|
|
385
|
-
let text = ''
|
|
386
|
-
for (const block of blocks) {
|
|
387
|
-
const editable = editRefusal(caller, block) === null
|
|
388
|
-
text += `\n<block label="${block.front.label}" chars="${charCount(block.body)}/${block.front.limit}"${editable ? '' : ' read-only="true"'}>\n${block.front.description}\n---\n${block.body.trim() || '(empty)'}\n</block>\n`
|
|
389
|
-
}
|
|
390
|
-
return text
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export type MemoryBlock = Omit<BlockFront, 'agents'> & { agents?: string[]; id: string; scope: BlockScope; owner: string | null; body: string; path: string; revisionId: string | null; canUndo: boolean }
|
|
394
|
-
|
|
395
|
-
export function response(wiki: string, block: Block): MemoryBlock {
|
|
396
|
-
const { agents, ...front } = block.front
|
|
397
|
-
return {
|
|
398
|
-
id: block.id,
|
|
399
|
-
scope: block.scope,
|
|
400
|
-
owner: block.owner,
|
|
401
|
-
...front,
|
|
402
|
-
...(agents.length ? { agents } : {}),
|
|
403
|
-
body: block.body,
|
|
404
|
-
path: block.path,
|
|
405
|
-
revisionId: store.currentRevision(wiki, block.id),
|
|
406
|
-
canUndo: lastEdit(wiki, block.id) !== null
|
|
407
|
-
}
|
|
408
|
-
}
|
package/src/memory/caller.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// Who is calling the memory and Wiki API. The embedding application supplies this when a
|
|
2
|
-
// session starts; the harness never looks an agent up by itself.
|
|
3
|
-
import { findProject, projectsOrNone, type Project } from '../core/projects'
|
|
4
|
-
import { invalid } from '../core/errors'
|
|
5
|
-
import type { HarnessEnv } from '../core/env'
|
|
6
|
-
import * as blocks from './blocks'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The current project only orders results and picks a default namespace for proposals; it
|
|
10
|
-
* never limits what can be read.
|
|
11
|
-
*/
|
|
12
|
-
export interface CallerContext {
|
|
13
|
-
/** Opaque, host-owned policy data frozen when this session starts. The harness only forwards it to the embedding app's tool policy. */
|
|
14
|
-
policyContext?: unknown
|
|
15
|
-
/**
|
|
16
|
-
* Trusted, frozen vault mutation capability supplied by an embedding host. `self` writes its own
|
|
17
|
-
* persona and notes directly and proposes the rest; `propose` writes nothing directly; `curator`
|
|
18
|
-
* applies proposals.
|
|
19
|
-
*/
|
|
20
|
-
vaultAccess?: 'self' | 'propose' | 'curator'
|
|
21
|
-
maintenanceJobId?: string | null
|
|
22
|
-
maintenanceJobIds?: string[]
|
|
23
|
-
runId?: string | null
|
|
24
|
-
sessionId?: string | null
|
|
25
|
-
projectId?: string | null
|
|
26
|
-
/** The agent calling; it decides which memory blocks are attached and editable. */
|
|
27
|
-
agentId?: string | null
|
|
28
|
-
/** Agents whose conversations this caller may not recall (opaque ids from the embedding host). */
|
|
29
|
-
recallExcludeAgents?: string[] | null
|
|
30
|
-
/** The agent's name, recorded on the revisions it makes. */
|
|
31
|
-
agentName?: string | null
|
|
32
|
-
/** Projects whose briefs the agent carries, most recently active first. */
|
|
33
|
-
briefProjectIds?: string[] | null
|
|
34
|
-
/** `Chat` for an agent's own conversation, `Code` for a session in a project. */
|
|
35
|
-
actor?: string | null
|
|
36
|
-
/** `cli` when the caller reaches the Wiki through the `xagent-wiki` command. */
|
|
37
|
-
surface?: string | null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const isSession = (context: CallerContext): boolean => context.actor === 'Code'
|
|
41
|
-
export const actorOf = (context: CallerContext): string => (isSession(context) ? 'Code' : 'Chat')
|
|
42
|
-
export const usesCli = (context: CallerContext): boolean => context.surface === 'cli'
|
|
43
|
-
|
|
44
|
-
export function projectOf(root: string, context: CallerContext): Project | null {
|
|
45
|
-
if (!context.projectId) return null
|
|
46
|
-
try {
|
|
47
|
-
return findProject(root, context.projectId)
|
|
48
|
-
} catch {
|
|
49
|
-
return null
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Who is asking, for block attachment and permissions: an agent, or a session. */
|
|
54
|
-
export function blockCaller(env: HarnessEnv, context: CallerContext): blocks.Caller {
|
|
55
|
-
if (isSession(context)) return { kind: 'session', projectSlug: projectOf(env.root, context)?.slug ?? null }
|
|
56
|
-
if (!context.agentId) throw invalid('Memory blocks need an agent or a Harness session.')
|
|
57
|
-
return { kind: 'agent', id: context.agentId, name: context.agentName ?? env.directory.agentName(context.agentId) ?? context.agentId, vaultAccess: context.vaultAccess === 'propose' ? 'propose' : context.vaultAccess ? 'self' : undefined }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** The blocks in the caller's context: an agent also carries the briefs of its active projects. */
|
|
61
|
-
export function callerBlocks(env: HarnessEnv, context: CallerContext): { caller: blocks.Caller; attached: blocks.Block[] } {
|
|
62
|
-
const caller = blockCaller(env, context)
|
|
63
|
-
let slugs: string[] = []
|
|
64
|
-
if (caller.kind === 'agent' && context.briefProjectIds?.length) {
|
|
65
|
-
const projects = projectsOrNone(env.root)
|
|
66
|
-
slugs = context.briefProjectIds.flatMap((id) => projects.find((project) => project.id === id)?.slug ?? [])
|
|
67
|
-
}
|
|
68
|
-
return { caller, attached: blocks.attached(env.root, caller, slugs) }
|
|
69
|
-
}
|