@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
|
@@ -3,17 +3,16 @@ import { tmpdir } from 'node:os'
|
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { afterEach, expect, test } from 'vitest'
|
|
5
5
|
|
|
6
|
-
import { dataRoot
|
|
6
|
+
import { dataRoot } from '../../tests/helpers'
|
|
7
|
+
import type { CallerContext } from '../core/caller'
|
|
7
8
|
import { createEnv, type HarnessEnv } from '../core/env'
|
|
8
9
|
import { writeJson } from '../core/fs'
|
|
9
10
|
import { closeIndex } from '../core/index'
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import { reindexWiki } from '../memory/wikiIndex'
|
|
16
|
-
import { compile, fit, launchText, load, memoryDelta, profileFor, profiles, record, REFERENCES_BUDGET, render, saveForSession, sessionFile, sessionLaunch, setProfile, type ContextPack, type ContextUpdate, type HostSection, type Launch } from './compile'
|
|
11
|
+
import { materialise } from '../tree/materialise'
|
|
12
|
+
import * as store from '../tree/store'
|
|
13
|
+
import type { Tree } from '../tree/surface'
|
|
14
|
+
import * as notes from '../wiki/notes'
|
|
15
|
+
import { compile, fit, load, render, sessionFile, sessionLaunch, SECTION_ORDER, type ContextUpdate, type HostSection, type Launch } from './compile'
|
|
17
16
|
|
|
18
17
|
const roots: string[] = []
|
|
19
18
|
afterEach(() => roots.splice(0).forEach(closeIndex))
|
|
@@ -21,33 +20,33 @@ afterEach(() => roots.splice(0).forEach(closeIndex))
|
|
|
21
20
|
function setup(): HarnessEnv {
|
|
22
21
|
const root = dataRoot()
|
|
23
22
|
roots.push(root)
|
|
24
|
-
writeJson(join(root, 'projects.json'), [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
ensureProjectNamespace(root, 'xingular-agent', 'Xingular Agent')
|
|
29
|
-
const wiki = wikiDir(root)
|
|
30
|
-
writeFileSync(join(wiki, 'projects/xingular-agent/decisions.md'), '---\nid: decisions\ntitle: Decisions\ntype: decision\n---\n\nTauri 2 for the shell.\n')
|
|
31
|
-
writeFileSync(join(wiki, 'facts/pnpm.md'), '---\nid: pnpm\ntitle: Use pnpm\nkind: preference\nnamespace: /\nconfidence: high\n---\n\nUse pnpm, not npm.\n')
|
|
32
|
-
writeFileSync(join(wiki, 'facts/on-call.md'), '---\nid: on-call\ntitle: On call this week\nkind: fact\nnamespace: /\nconfidence: high\nvolatile: true\n---\n\nAstro team is on call.\n')
|
|
33
|
-
writeFileSync(join(wiki, 'facts/site-only.md'), '---\nid: site-only\ntitle: Site uses Astro\nkind: fact\nnamespace: /projects/site\n---\n\nAstro renders the site.\n')
|
|
34
|
-
writeFile(join(root, 'registry/rules/tests.md'), '---\nname: Run tests\n---\n\nAlways run the tests.\n')
|
|
35
|
-
writeFile(join(root, 'registry/skills/review/SKILL.md'), '---\nname: review\ndescription: Review a change\n---\n\nRead the diff first.\n')
|
|
36
|
-
writeJson(join(root, 'config/registry-enabled.json'), { global: ['rules/tests'], p1: ['skills/review'] })
|
|
23
|
+
writeJson(join(root, 'projects.json'), [{ id: 'p1', name: 'Xingular Agent', rootPath: '/src/xingular-agent', autoMaintenance: true, createdAt: 't', slug: 'xingular-agent' }])
|
|
24
|
+
notes.ensureLayout(root, 'Test')
|
|
25
|
+
notes.write(root, 'projects/xingular-agent/decisions', '---\ntitle: Decisions\n---\n\nTauri 2 for the shell. See [[library/glossary]].\n')
|
|
26
|
+
notes.write(root, 'library/glossary', '# Glossary\n\nA harness is the system around a model.\n')
|
|
37
27
|
mkdirSync(join(root, 'runs/run-1'), { recursive: true })
|
|
38
|
-
reindexWiki(root)
|
|
39
28
|
return createEnv(root)
|
|
40
29
|
}
|
|
41
30
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
31
|
+
const tree: Tree = {
|
|
32
|
+
'systemprompt.md': 'You draft release notes.\n',
|
|
33
|
+
'guardrails.md': 'Never publish without a review.\n- When writing a changelog, load the `changelog` skill.\n',
|
|
34
|
+
'memory.md': 'Releases ship on Tuesdays.\n',
|
|
35
|
+
'tool_guidance.md': 'Prefer grep over reading whole files.\n',
|
|
36
|
+
'skills/changelog/SKILL.md': '---\nname: changelog\ndescription: writing a changelog\n---\nGroup by type.\n',
|
|
37
|
+
'sub_agents/reader/AGENT.md': '---\ndescription: reads long logs and answers briefly\n---\nRead the log and answer in three lines.\n',
|
|
38
|
+
'scripts/count.py': 'print(1)\n'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const agent = (extra: Partial<CallerContext> = {}): CallerContext => ({ agentId: 'nora', agentName: 'Nora', projectId: 'p1', runId: 'run-1', actor: 'Chat', ...extra })
|
|
42
|
+
const identity: HostSection = { id: 'identity', title: 'Identity', body: 'You are Nora.', budget: 1000, order: 10 }
|
|
43
|
+
const work = (body: string): HostSection => ({ id: 'work', title: 'Active work', body, budget: 1000, order: 70, dynamic: true })
|
|
44
|
+
const hosted = (extra: Partial<Launch> = {}): Launch => ({ sections: [identity, work('You have no unresolved work.')], tools: ['read'], ...extra })
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const hosted = (extra: Partial<Launch> = {}): Launch => ({ sections: [identity, role, work('You have no unresolved work.\n')], ...extra })
|
|
46
|
+
function scoped(env: HarnessEnv) {
|
|
47
|
+
store.seed(env.root, 'nora', tree)
|
|
48
|
+
return materialise(env.root, { scope: 'nora' }, { subagents: true })
|
|
49
|
+
}
|
|
51
50
|
|
|
52
51
|
test('cuts at a paragraph and points to the rest', () => {
|
|
53
52
|
expect(fit('short', 100, '(more)')).toEqual({ body: 'short', cut: false })
|
|
@@ -56,126 +55,46 @@ test('cuts at a paragraph and points to the rest', () => {
|
|
|
56
55
|
expect(cut.body).toBe(`${'a'.repeat(40)}\n\n${'b'.repeat(40)}\n\n(more)`)
|
|
57
56
|
})
|
|
58
57
|
|
|
59
|
-
test('
|
|
60
|
-
const env = setup()
|
|
61
|
-
setProfile(env.root, 'nora', 'off')
|
|
62
|
-
const revision = references.pin(env.root, 'skill', 'skills/review')
|
|
63
|
-
writeFileSync(join(env.root, 'registry/skills/review/SKILL.md'), 'Changed instructions.')
|
|
64
|
-
const newer = references.pin(env.root, 'skill', 'skills/review')
|
|
65
|
-
const caller = agent({ runId: 'run-1' })
|
|
66
|
-
const pack = compile(env, caller, hosted({ references: [{ kind: 'skill', id: 'skills/review', label: 'Review', revision }] }))
|
|
67
|
-
expect(ids(pack)).toEqual(['identity', 'role', 'references'])
|
|
68
|
-
const selected = pack.sections.find((entry) => entry.id === 'references')!
|
|
69
|
-
expect(selected.body).toContain('Read the diff first.')
|
|
70
|
-
expect(selected.body).not.toContain('Changed instructions.')
|
|
71
|
-
expect(revision).not.toBe(newer)
|
|
72
|
-
record(env.root, caller, pack)
|
|
73
|
-
expect(references.readPinned(env.root, 'run-1', 'skills/review', revision, 0)).toContain('Read the diff first.')
|
|
74
|
-
expect(() => references.readPinned(env.root, 'run-1', 'skills/review', newer, 0)).toThrow()
|
|
75
|
-
expect(readRunContext(env.root, 'run-1')[0].revisionId).toBe(revision)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
test('application lines sit with the references and large sources keep retrieval pointers', () => {
|
|
58
|
+
test('the harness tree supplies the instructions, guardrails, memory, subagents, scripts and tool guidance, in order', () => {
|
|
79
59
|
const env = setup()
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const selected = pack.sections.find((entry) => entry.id === 'references')!
|
|
84
|
-
expect(selected.body).toContain('Agent messaging is unavailable')
|
|
85
|
-
expect(selected.cut).toBe(true)
|
|
86
|
-
expect(selected.size).toBeLessThanOrEqual(REFERENCES_BUDGET)
|
|
87
|
-
expect(selected.body).toContain('wiki_read(id="skills/review", revision=')
|
|
88
|
-
expect(selected.sources[0].revision).toBe(revision)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test('a session gets blocks, rules, skills, knowledge and tools', () => {
|
|
92
|
-
const env = setup()
|
|
93
|
-
const pack = compile(env, session('p1'), { connections: 'Use get_capabilities.' })
|
|
94
|
-
expect(pack.profile).toBe('session')
|
|
95
|
-
expect(ids(pack)).toEqual(['blocks', 'rules', 'skills', 'knowledge', 'tools'])
|
|
60
|
+
const pack = compile(env, agent(), hosted(), scoped(env))
|
|
61
|
+
const ids = pack.sections.map((entry) => entry.id)
|
|
62
|
+
expect(ids).toEqual(['identity', 'system', 'guardrails', 'memory', 'subagents', 'scripts', 'work', 'wiki', 'tools'])
|
|
96
63
|
const text = render(env, pack)
|
|
97
|
-
|
|
98
|
-
expect(text).
|
|
99
|
-
|
|
100
|
-
expect(
|
|
101
|
-
expect(
|
|
64
|
+
expect(text).toContain('You draft release notes.')
|
|
65
|
+
expect(text).toContain('Never publish without a review.')
|
|
66
|
+
expect(text).toContain('Releases ship on Tuesdays.')
|
|
67
|
+
expect(text).toContain('- reader — reads long logs and answers briefly')
|
|
68
|
+
expect(text).toMatch(/scripts: count\.py/)
|
|
69
|
+
expect(text).toContain('### How to use your tools\n\nPrefer grep over reading whole files.')
|
|
70
|
+
expect(pack.harness).toEqual({ scope: 'nora', fingerprint: store.head(env.root, 'nora')!.fingerprint })
|
|
71
|
+
expect(pack.sections.find((entry) => entry.id === 'system')!.sources).toEqual([{ id: 'nora', kind: 'harness', revision: pack.harness!.fingerprint }])
|
|
102
72
|
})
|
|
103
73
|
|
|
104
|
-
test('
|
|
74
|
+
test('without a tree, only the application, the Wiki and the tools speak', () => {
|
|
105
75
|
const env = setup()
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
expect(rules.cut && rules.size <= 3000 && rules.body.endsWith('wiki_list kind rule.)')).toBe(true)
|
|
110
|
-
expect(ids(pack)).toEqual(['blocks', 'rules', 'skills', 'knowledge', 'tools'])
|
|
111
|
-
expect(pack.sections.filter((entry) => entry.id !== 'rules').every((entry) => !entry.cut)).toBe(true)
|
|
76
|
+
const pack = compile(env, agent(), hosted())
|
|
77
|
+
expect(pack.sections.map((entry) => entry.id)).toEqual(['identity', 'work', 'wiki', 'tools'])
|
|
78
|
+
expect(pack.harness).toBeNull()
|
|
112
79
|
})
|
|
113
80
|
|
|
114
|
-
test('
|
|
81
|
+
test('the Wiki section lists the current project\'s notes and only describes wiki_write when it is on', () => {
|
|
115
82
|
const env = setup()
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{ group: 'Agents', tools: ['list_agents', 'get_agent'], guidance: 'Agents (list_agents, get_agent): see and message the other agents.', order: 40 },
|
|
122
|
-
{ group: 'Browser', tools: [], guidance: 'Browser: act in the managed browser.', order: 50 },
|
|
123
|
-
{ group: 'Files', tools: ['files_list', 'files_read'], guidance: 'Saved files (files_list, files_read): read the shared vault.', order: 80 }
|
|
124
|
-
]
|
|
125
|
-
}))
|
|
126
|
-
expect(pack.toolGroups.map((group) => group.group)).toEqual(['Memory', 'Knowledge', 'Work', 'Agents', 'Native', 'Files'])
|
|
127
|
-
expect(pack.toolGroups[3].tools).toEqual(['list_agents', 'get_agent'])
|
|
128
|
-
const tools = pack.sections.at(-1)!.body
|
|
129
|
-
expect(tools).toContain('Work (dispatch_work)')
|
|
130
|
-
expect(tools).toContain('Files (read)')
|
|
131
|
-
expect(tools).not.toContain('Browser')
|
|
132
|
-
expect(tools).not.toContain('connected apps')
|
|
133
|
-
expect(pack.profile).toBe('agent')
|
|
134
|
-
expect(ids(pack)).toEqual(['identity', 'role', 'blocks', 'rules', 'skills', 'work', 'knowledge', 'tools'])
|
|
135
|
-
const text = render(env, pack)
|
|
136
|
-
for (const expected of ['# Xingular Harness context\n\nYou are "Nora", an Xingular Agent agent.\n\n## Role', '<block label="persona"', '<block label="notes"', 'You have no unresolved work.', '- /projects/xingular-agent', 'Site uses Astro']) expect(text).toContain(expected)
|
|
137
|
-
// A volatile fact is never carried, even when it matches.
|
|
138
|
-
expect(text).not.toContain('On call this week')
|
|
139
|
-
// A skill enabled for one project is not listed for an agent, only pointed to.
|
|
140
|
-
expect(text).not.toContain('review (skills/review)')
|
|
83
|
+
const text = render(env, compile(env, agent(), hosted()))
|
|
84
|
+
expect(text).toContain('- projects/xingular-agent/decisions — Decisions')
|
|
85
|
+
expect(text).toContain('wiki_search, wiki_read and wiki_list read project notes.')
|
|
86
|
+
expect(text).not.toContain('wiki_write')
|
|
87
|
+
expect(render(env, compile(env, agent(), hosted({ tools: ['read', 'wiki_write'] })))).toContain('wiki_write saves one')
|
|
141
88
|
})
|
|
142
89
|
|
|
143
|
-
test('
|
|
90
|
+
test('selected notes are references for this request only', () => {
|
|
144
91
|
const env = setup()
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
expect(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
expect(
|
|
151
|
-
expect(note).toContain('<block label="project/xingular-agent"')
|
|
152
|
-
expect(note).toContain('Uses pnpm.')
|
|
153
|
-
expect(note).not.toContain('<block label="user"')
|
|
154
|
-
expect(memoryDelta(env, directory, session('p1'))).toBeNull()
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
test('a profile limits the sections and is recorded', () => {
|
|
158
|
-
const env = setup()
|
|
159
|
-
expect(profileFor(env.root, session('p1'))).toBe('full')
|
|
160
|
-
setProfile(env.root, 's1', 'minimal')
|
|
161
|
-
const pack = compile(env, session('p1'))
|
|
162
|
-
expect([pack.contextProfile, ids(pack)]).toEqual(['minimal', ['rules', 'tools']])
|
|
163
|
-
setProfile(env.root, 's1', 'off')
|
|
164
|
-
const off = compile(env, session('p1'))
|
|
165
|
-
expect([off.sections, off.toolGroups]).toEqual([[], []])
|
|
166
|
-
setProfile(env.root, 's1', 'full')
|
|
167
|
-
expect(profiles(env.root)).toEqual({})
|
|
168
|
-
expect(() => setProfile(env.root, '../x', 'off')).toThrow()
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
test('the pack is recorded with the run', () => {
|
|
172
|
-
const env = setup()
|
|
173
|
-
const text = launchText(env, session('p1'))
|
|
174
|
-
const saved = load(env.root, 'run-1')!
|
|
175
|
-
expect(render(env, saved)).toBe(text)
|
|
176
|
-
expect(saved.sections.some((entry) => entry.id === 'blocks' && entry.sources.some((source) => source.id === 'blocks/shared/user'))).toBe(true)
|
|
177
|
-
expect(readRunContext(env.root, 'run-1').some((entry) => entry.id === 'rules/tests' && entry.via === 'entrypoint')).toBe(true)
|
|
178
|
-
expect(load(env.root, 'missing')).toBeNull()
|
|
92
|
+
const launch = hosted({ references: [{ kind: 'note', path: 'library/glossary' }, { kind: 'line', id: 'l1', label: 'Line', text: 'The user pinned a screenshot.' }] })
|
|
93
|
+
const section = compile(env, agent(), launch).sections.find((entry) => entry.id === 'references')!
|
|
94
|
+
expect(section.body).toContain('The user pinned a screenshot.')
|
|
95
|
+
expect(section.body).toContain('A harness is the system around a model.')
|
|
96
|
+
expect(section.sources[0]).toMatchObject({ id: 'library/glossary', kind: 'note' })
|
|
97
|
+
expect(SECTION_ORDER.references).toBeLessThan(SECTION_ORDER.wiki)
|
|
179
98
|
})
|
|
180
99
|
|
|
181
100
|
function delivered(path: string, update: ContextUpdate): void {
|
|
@@ -184,64 +103,28 @@ function delivered(path: string, update: ContextUpdate): void {
|
|
|
184
103
|
|
|
185
104
|
test('state stays out of the system prompt and only delivered changes are resent', () => {
|
|
186
105
|
const env = setup()
|
|
187
|
-
const
|
|
188
|
-
const first = sessionLaunch(env,
|
|
189
|
-
|
|
190
|
-
expect(first.system).not.toContain('<block')
|
|
106
|
+
const materialised = scoped(env)
|
|
107
|
+
const first = sessionLaunch(env, agent(), hosted(), null, materialised)
|
|
108
|
+
expect(first.system).toContain('You draft release notes.')
|
|
191
109
|
expect(first.system).not.toContain('You have no unresolved work.')
|
|
192
|
-
expect(first.
|
|
193
|
-
expect(
|
|
194
|
-
expect(initial.content).toContain('You have no unresolved work.')
|
|
195
|
-
// Recording a prepared launch is not evidence that the session received its context.
|
|
196
|
-
expect(sessionLaunch(env, caller, hosted(), null).update).not.toBeNull()
|
|
110
|
+
expect(first.update!.content).toContain('You have no unresolved work.')
|
|
111
|
+
expect(first.update!.content).toContain('## Wiki')
|
|
197
112
|
const history = join(mkdtempSync(join(tmpdir(), 'harness-history-')), 'chat.jsonl')
|
|
198
|
-
delivered(history,
|
|
199
|
-
const unchanged = sessionLaunch(env,
|
|
113
|
+
delivered(history, first.update!)
|
|
114
|
+
const unchanged = sessionLaunch(env, agent(), hosted(), history, materialised)
|
|
200
115
|
expect(unchanged.system).toBe(first.system)
|
|
201
116
|
expect(unchanged.update).toBeNull()
|
|
202
|
-
|
|
203
|
-
const changed = sessionLaunch(env,
|
|
117
|
+
notes.write(env.root, 'projects/xingular-agent/release', '# Release checklist\n')
|
|
118
|
+
const changed = sessionLaunch(env, agent(), hosted(), history, materialised)
|
|
204
119
|
expect(changed.system).toBe(first.system)
|
|
205
|
-
expect(changed.update!.content).toContain('
|
|
120
|
+
expect(changed.update!.content).toContain('projects/xingular-agent/release')
|
|
206
121
|
expect(changed.update!.content).not.toContain('## Active work')
|
|
207
|
-
expect(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
expect(
|
|
212
|
-
expect(
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
test('pinned application text reaches native history with context off without changing the system prefix', () => {
|
|
216
|
-
const env = setup()
|
|
217
|
-
setProfile(env.root, 'nora', 'off')
|
|
218
|
-
const before = sessionLaunch(env, agent(), hosted(), null)
|
|
219
|
-
const after = sessionLaunch(env, agent(), { sections: [identity, role, work('You have no unresolved work.\n', 'Recent task reports:\n\nTask t1: Reviewed evidence shown to the user\n')] }, null)
|
|
220
|
-
expect(after.system).toBe(before.system)
|
|
221
|
-
expect(after.update!.content).toContain('Reviewed evidence shown to the user')
|
|
222
|
-
expect(after.update!.content).not.toContain('You have no unresolved work.')
|
|
223
|
-
expect(before.update!.content).not.toContain('Reviewed evidence')
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
test('references apply to each request and profile changes withdraw old state', () => {
|
|
227
|
-
const env = setup()
|
|
228
|
-
const revision = references.pin(env.root, 'skill', 'skills/review')
|
|
229
|
-
const launch = hosted({ references: [{ kind: 'skill', id: 'skills/review', label: 'Review', revision }] })
|
|
230
|
-
const first = sessionLaunch(env, agent(), launch, null)
|
|
231
|
-
expect(first.system).not.toContain('Read the diff first.')
|
|
232
|
-
const history = join(mkdtempSync(join(tmpdir(), 'harness-history-')), 'chat.jsonl')
|
|
233
|
-
delivered(history, first.update!)
|
|
234
|
-
const repeated = sessionLaunch(env, agent(), launch, history).update!
|
|
235
|
-
expect(repeated.content).toContain('Read the diff first.')
|
|
236
|
-
expect(repeated.content).not.toContain('## Memory blocks')
|
|
237
|
-
const removed = sessionLaunch(env, agent(), hosted(), history).update!
|
|
238
|
-
expect(removed.content).toContain('## references')
|
|
239
|
-
expect(removed.content).not.toContain('Read the diff first.')
|
|
240
|
-
setProfile(env.root, 'nora', 'off')
|
|
241
|
-
const withdrawn = sessionLaunch(env, agent(), hosted(), history).update!
|
|
242
|
-
expect(withdrawn.snapshot).toEqual([])
|
|
243
|
-
for (const id of ['blocks', 'references', 'work', 'knowledge']) expect(withdrawn.content).toContain(`## ${id}`)
|
|
244
|
-
expect(sessionLaunch(env, agent(), launch, history).update!.content).toContain('Read the diff first.')
|
|
122
|
+
expect(load(env.root, 'run-1')!.delivery!.updatedSectionIds).toEqual(['wiki'])
|
|
123
|
+
// A new harness version changes the stable prefix, never the delivered state.
|
|
124
|
+
store.commit(env.root, 'nora', { ...tree, 'memory.md': 'Releases ship on Thursdays.\n' }, { kind: 'edit' })
|
|
125
|
+
const next = sessionLaunch(env, agent(), hosted(), history, materialise(env.root, { scope: 'nora' }, { subagents: true }))
|
|
126
|
+
expect(next.system).toContain('Releases ship on Thursdays.')
|
|
127
|
+
expect(next.system).not.toBe(first.system)
|
|
245
128
|
})
|
|
246
129
|
|
|
247
130
|
test('the snapshot follows the active branch and refreshes after compaction', () => {
|
|
@@ -251,11 +134,17 @@ test('the snapshot follows the active branch and refreshes after compaction', ()
|
|
|
251
134
|
const history = join(directory, 'chat.jsonl')
|
|
252
135
|
delivered(history, initial)
|
|
253
136
|
const original = readFileSync(history, 'utf8')
|
|
254
|
-
// An unrelated branch's snapshot must not decide what this branch already received.
|
|
255
137
|
writeFileSync(history, `${original}${JSON.stringify({ type: 'custom_message', id: 'other', parentId: null, customType: 'xingular-context', details: { xingularContext: [] } })}\n${JSON.stringify({ type: 'message', id: 'leaf', parentId: 'context-1' })}\n`)
|
|
256
138
|
expect(sessionLaunch(env, agent(), hosted(), history).update).toBeNull()
|
|
257
139
|
writeFileSync(history, `${original}${JSON.stringify({ type: 'compaction', id: 'summary', parentId: 'context-1' })}\n`)
|
|
258
|
-
expect(sessionLaunch(env, agent(), hosted(), history).update!.content).toContain('##
|
|
140
|
+
expect(sessionLaunch(env, agent(), hosted(), history).update!.content).toContain('## Wiki')
|
|
259
141
|
expect(sessionFile(directory, join(directory, 'older.jsonl'))).toBe(join(directory, 'older.jsonl'))
|
|
260
142
|
expect(sessionFile(directory)).toBe(history)
|
|
261
143
|
})
|
|
144
|
+
|
|
145
|
+
test('a subagent section is left out when the session cannot start subagents', () => {
|
|
146
|
+
const env = setup()
|
|
147
|
+
store.seed(env.root, 'nora', tree)
|
|
148
|
+
const pack = compile(env, agent(), hosted(), materialise(env.root, { scope: 'nora' }, { subagents: false }))
|
|
149
|
+
expect(pack.sections.some((entry) => entry.id === 'subagents')).toBe(false)
|
|
150
|
+
})
|