@xingular/harness 0.1.3 → 0.1.5
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/README.md +7 -3
- package/dist/cli.js +270 -125
- package/package.json +1 -1
- package/src/accounts/accounts.test.ts +1 -1
- package/src/accounts/env.test.ts +1 -1
- package/src/accounts/env.ts +1 -1
- package/src/core/path.test.ts +14 -0
- package/src/core/path.ts +56 -22
- package/src/mcp/connections.ts +2 -2
- package/src/mcp/manager.ts +2 -2
- package/src/memory/blocks.ts +2 -2
- package/src/memory/caller.ts +1 -1
- package/src/memory/dispatch.ts +1 -1
- package/src/memory/facts.ts +1 -1
- package/src/memory/store.ts +1 -1
- package/src/prompt/compile.test.ts +2 -2
- package/src/prompt/compile.ts +2 -2
- package/src/sessions/contracts.ts +12 -1
- package/src/sessions/runner.ts +16 -3
- package/src/sessions/sessions.test.ts +21 -0
- package/src/terminal/cli.ts +5 -5
- package/src/terminal/client.ts +2 -2
- package/src/terminal/launch.test.ts +34 -0
- package/src/terminal/launch.ts +88 -0
- package/src/terminal/socket.test.ts +44 -1
- package/src/terminal/socket.ts +32 -12
- package/src/terminal/standalone.test.ts +10 -0
- package/src/terminal/standalone.ts +11 -1
- package/src/terminal/terminal.test.ts +33 -5
- package/src/terminal/terminals.ts +32 -18
- package/src/tools/definitions.ts +2 -2
- package/src/tools/types.ts +3 -0
package/src/terminal/socket.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// The session socket: how a process the harness did not start in-process (the `pi` command in
|
|
2
|
-
// a terminal, with its extension and the `
|
|
3
|
-
// session it belongs to. Owner-only Unix socket, newline-delimited JSON,
|
|
4
|
-
// connection. Each terminal session holds a random token bound to its caller, its
|
|
5
|
-
// grant, and its controller; the token stops working when the terminal ends.
|
|
6
|
-
import { createHash } from 'node:crypto'
|
|
2
|
+
// a terminal, with its extension and the `xagent-wiki` / `xagent-mcp` commands) reaches the
|
|
3
|
+
// session it belongs to. Owner-only Unix socket (a named pipe on Windows), newline-delimited JSON,
|
|
4
|
+
// one request per connection. Each terminal session holds a random token bound to its caller, its
|
|
5
|
+
// connected-apps grant, and its controller; the token stops working when the terminal ends.
|
|
6
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
7
7
|
import { chmodSync, mkdirSync, rmSync, statSync } from 'node:fs'
|
|
8
8
|
import { createServer, type Server, type Socket } from 'node:net'
|
|
9
9
|
import { tmpdir } from 'node:os'
|
|
@@ -42,16 +42,32 @@ const MAX_REQUEST_BYTES = 1024 * 1024
|
|
|
42
42
|
/** macOS refuses a socket path longer than 103 bytes. */
|
|
43
43
|
const MAX_SOCKET_PATH = 100
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Windows has no socket files for Node's `net`: it listens on a named pipe, which lives in a
|
|
47
|
+
* machine-wide namespace. A random name keeps another local process from guessing it and standing
|
|
48
|
+
* in front of the session; the token still decides what a caller may do.
|
|
49
|
+
*/
|
|
45
50
|
export function socketPath(stateDir: string): string {
|
|
51
|
+
if (process.platform === 'win32') return `\\\\.\\pipe\\xingular-harness-${randomBytes(12).toString('hex')}`
|
|
46
52
|
const path = join(stateDir, 'run', 'harness.sock')
|
|
47
53
|
if (Buffer.byteLength(path) <= MAX_SOCKET_PATH) return path
|
|
48
54
|
return join(tmpdir(), `xh-${createHash('sha256').update(path).digest('hex').slice(0, 16)}.sock`)
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
/** A named pipe has no folder, file mode, or owner to check or clean up. */
|
|
58
|
+
const isPipePath = (path: string): boolean => path.startsWith('\\\\.\\pipe\\')
|
|
59
|
+
|
|
51
60
|
/** Names the folder and the user when the operating system refuses the socket, so the cause shows. */
|
|
52
61
|
function refusedBind(path: string, error: unknown): unknown {
|
|
53
62
|
const code = (error as { code?: unknown } | null)?.code
|
|
54
63
|
if (code !== 'EACCES' && code !== 'EPERM') return error
|
|
64
|
+
if (isPipePath(path)) {
|
|
65
|
+
return new HarnessError(
|
|
66
|
+
'session_socket_denied',
|
|
67
|
+
`The session pipe ${path} could not be created (${code}).`,
|
|
68
|
+
'Start the harness again from your own user session. If it keeps failing, report this code and the Windows version.'
|
|
69
|
+
)
|
|
70
|
+
}
|
|
55
71
|
let folder: string
|
|
56
72
|
try {
|
|
57
73
|
const stat = statSync(dirname(path))
|
|
@@ -169,18 +185,22 @@ export class SessionSocket {
|
|
|
169
185
|
/** Binds the socket once; later calls wait for the same bind. */
|
|
170
186
|
listen(): Promise<void> {
|
|
171
187
|
this.listening ??= new Promise<void>((resolve, reject) => {
|
|
172
|
-
|
|
173
|
-
|
|
188
|
+
if (!isPipePath(this.path)) {
|
|
189
|
+
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 })
|
|
190
|
+
rmSync(this.path, { force: true })
|
|
191
|
+
}
|
|
174
192
|
const server = createServer((client) => this.serve(client))
|
|
175
193
|
server.once('error', (error) => {
|
|
176
194
|
this.listening = null
|
|
177
195
|
reject(refusedBind(this.path, error))
|
|
178
196
|
})
|
|
179
197
|
server.listen(this.path, () => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
198
|
+
if (!isPipePath(this.path)) {
|
|
199
|
+
try {
|
|
200
|
+
chmodSync(this.path, 0o600)
|
|
201
|
+
} catch (error) {
|
|
202
|
+
log('session socket permissions', error instanceof Error ? error.message : String(error))
|
|
203
|
+
}
|
|
184
204
|
}
|
|
185
205
|
this.server = server
|
|
186
206
|
resolve()
|
|
@@ -196,6 +216,6 @@ export class SessionSocket {
|
|
|
196
216
|
this.listening = null
|
|
197
217
|
for (const client of this.clients) client.destroy()
|
|
198
218
|
if (server) await new Promise<void>((resolve) => server.close(() => resolve()))
|
|
199
|
-
rmSync(this.path, { force: true })
|
|
219
|
+
if (!isPipePath(this.path)) rmSync(this.path, { force: true })
|
|
200
220
|
}
|
|
201
221
|
}
|
|
@@ -117,4 +117,14 @@ describe('standalone terminal startup', () => {
|
|
|
117
117
|
expect(existsSync(socketPath(harness.stateDir))).toBe(false)
|
|
118
118
|
expect(harness.terminals.has('failed')).toBe(false)
|
|
119
119
|
})
|
|
120
|
+
|
|
121
|
+
it('explains a missing Pi executable in terms of the terminal launch', async () => {
|
|
122
|
+
const home = temporaryDirectory('harness-standalone-enoent-')
|
|
123
|
+
const cwd = temporaryDirectory('harness-cwd-enoent-')
|
|
124
|
+
const error = Object.assign(new Error('spawn /opt/pi/bin/pi ENOENT'), { code: 'ENOENT' })
|
|
125
|
+
const spawn = fakeSpawn(() => undefined, { error })
|
|
126
|
+
|
|
127
|
+
await expect(runStandaloneTerminal([], { home, cwd, stdinIsTTY: true, stdoutIsTTY: true, createSessionId: () => 'missing-pi', createHarness: harnessFactory, spawn }))
|
|
128
|
+
.rejects.toThrow(/Could not start Pi terminal.*Pi executable.*\/opt\/pi\/bin\/pi/)
|
|
129
|
+
})
|
|
120
130
|
})
|
|
@@ -73,6 +73,16 @@ function exitCode(code: number | null, signal: NodeJS.Signals | null): number {
|
|
|
73
73
|
return 1
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
function spawnError(error: Error, launch: TerminalLaunch): Error {
|
|
77
|
+
const code = (error as NodeJS.ErrnoException).code
|
|
78
|
+
if (code !== 'ENOENT' && code !== 'EACCES' && code !== 'EPERM') return error
|
|
79
|
+
if (launch.program === process.execPath || launch.env.XINGULAR_HARNESS_NODE === launch.program) {
|
|
80
|
+
return new Error(`Could not start the Pi terminal because Node could not run "${launch.args[0] ?? 'the bundled Pi CLI'}" (${code}). Verify Node and the bundled Pi package are installed.`, { cause: error })
|
|
81
|
+
}
|
|
82
|
+
const problem = code === 'ENOENT' ? 'was not found' : `could not start (${code})`
|
|
83
|
+
return new Error(`Could not start Pi terminal. Pi executable "${launch.program}" ${problem}; verify it is installed and executable or check the configured terminal executable.`, { cause: error })
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
function spawnPi(launch: TerminalLaunch, cwd: string, spawn = nodeSpawn): Promise<number> {
|
|
77
87
|
return new Promise((resolveExit, reject) => {
|
|
78
88
|
const child = spawn(launch.program, launch.args, {
|
|
@@ -103,7 +113,7 @@ function spawnPi(launch: TerminalLaunch, cwd: string, spawn = nodeSpawn): Promis
|
|
|
103
113
|
process.on('SIGINT', onInterrupt)
|
|
104
114
|
process.on('SIGTERM', onTerminate)
|
|
105
115
|
process.on('SIGHUP', onHangup)
|
|
106
|
-
child.once('error', (error) => finish(() => reject(error)))
|
|
116
|
+
child.once('error', (error) => finish(() => reject(spawnError(error, launch))))
|
|
107
117
|
child.once('exit', (code, signal) => finish(() => resolveExit(exitCode(code, signal))))
|
|
108
118
|
})
|
|
109
119
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync } from 'node:fs'
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
2
3
|
import { connect } from 'node:net'
|
|
3
4
|
import { tmpdir } from 'node:os'
|
|
4
5
|
import { join } from 'node:path'
|
|
@@ -24,6 +25,10 @@ afterEach(async () => {
|
|
|
24
25
|
async function setup(policy: ToolPolicy = {}) {
|
|
25
26
|
const root = rootWithProject()
|
|
26
27
|
const harness = createHarness({ dataRoot: root, stateDir: mkdtempSync(join(tmpdir(), 'hs-')), secrets: memorySecretStore(), policy, piProgram: () => '/opt/pi/bin/pi', assets: { node: '/opt/node', cli: '/opt/harness/cli.js', piExtension: '/opt/harness/terminal-pi.js' } })
|
|
28
|
+
const legacyBin = join(harness.stateDir, 'run', 'bin')
|
|
29
|
+
mkdirSync(legacyBin, { recursive: true })
|
|
30
|
+
writeFileSync(join(legacyBin, 'agentx-wiki'), 'old wrapper')
|
|
31
|
+
writeFileSync(join(legacyBin, 'agentx-mcp'), 'old wrapper')
|
|
27
32
|
open.push(harness)
|
|
28
33
|
const account = harness.accounts.create('Main', 'apiKey').accounts[0]
|
|
29
34
|
const events: RunEvent[] = []
|
|
@@ -59,7 +64,7 @@ describe('a pi terminal session', () => {
|
|
|
59
64
|
expect(launch.accountId).toBe(account.id)
|
|
60
65
|
expect(launch.args[0]).toBe('--append-system-prompt')
|
|
61
66
|
expect(launch.args[1]).toContain('# Xingular Harness context')
|
|
62
|
-
expect(launch.args[1]).toContain('
|
|
67
|
+
expect(launch.args[1]).toContain('xagent-wiki')
|
|
63
68
|
// Pi's own discovery is off; its command's built-in extensions are named, and the harness's comes last.
|
|
64
69
|
expect(launch.args.slice(2)).toEqual([
|
|
65
70
|
'--model', 'chosen-model', '--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes',
|
|
@@ -68,13 +73,36 @@ describe('a pi terminal session', () => {
|
|
|
68
73
|
])
|
|
69
74
|
expect(launch.env.PI_CODING_AGENT_DIR).toBe(join(harness.accounts.dir, account.id, 'home', 'pi'))
|
|
70
75
|
expect(String(launch.env.PATH).split(':')[0]).toBe(join(harness.stateDir, 'run', 'bin'))
|
|
71
|
-
expect(readFileSync(join(harness.stateDir, 'run', 'bin', '
|
|
72
|
-
expect(readFileSync(join(harness.stateDir, 'run', 'bin', '
|
|
76
|
+
expect(readFileSync(join(harness.stateDir, 'run', 'bin', 'xagent-wiki'), 'utf8')).toBe("#!/bin/sh\nexec '/opt/node' '/opt/harness/cli.js' wiki \"$@\"\n")
|
|
77
|
+
expect(readFileSync(join(harness.stateDir, 'run', 'bin', 'xagent-mcp'), 'utf8')).toContain("'/opt/harness/cli.js' mcp")
|
|
78
|
+
expect(existsSync(join(harness.stateDir, 'run', 'bin', 'agentx-wiki'))).toBe(false)
|
|
79
|
+
expect(existsSync(join(harness.stateDir, 'run', 'bin', 'agentx-mcp'))).toBe(false)
|
|
73
80
|
// The session keeps the pack it started with.
|
|
74
81
|
expect(JSON.parse(readFileSync(join(sessionFolder, 'context.json'), 'utf8')).profile).toBe('session')
|
|
75
82
|
expect(root).toBeTruthy()
|
|
76
83
|
})
|
|
77
84
|
|
|
85
|
+
it.skipIf(process.platform !== 'win32')('starts the resolved Pi CLI through Node without a model or interactive terminal', async () => {
|
|
86
|
+
const root = rootWithProject()
|
|
87
|
+
const stateDir = mkdtempSync(join(tmpdir(), 'hs-pi-smoke-'))
|
|
88
|
+
const harness = createHarness({ dataRoot: root, stateDir, secrets: memorySecretStore() })
|
|
89
|
+
open.push(harness)
|
|
90
|
+
harness.accounts.create('Main', 'subscription')
|
|
91
|
+
const launch = await harness.terminals.prepare({ sessionId: 'pi-smoke', cwd: root, sessionFolder: join(root, 'sessions', 'pi-smoke') })
|
|
92
|
+
const cli = launch.args[0]
|
|
93
|
+
const pathKeys = Object.keys(launch.env).filter((name) => name.toLowerCase() === 'path')
|
|
94
|
+
|
|
95
|
+
expect(launch.program).toBe(process.execPath)
|
|
96
|
+
expect(pathKeys).toHaveLength(1)
|
|
97
|
+
expect(launch.env[pathKeys[0]]).toContain(';')
|
|
98
|
+
expect(launch.env.XINGULAR_HARNESS_NODE).toBe(process.execPath)
|
|
99
|
+
expect(readFileSync(join(stateDir, 'run', 'bin', 'xagent-wiki.cmd'), 'utf8')).toContain(' wiki %*\r\n')
|
|
100
|
+
const result = spawnSync(launch.program, [cli, '--help'], { cwd: root, encoding: 'utf8', timeout: 20_000 })
|
|
101
|
+
expect(result.error).toBeUndefined()
|
|
102
|
+
expect(result.status).toBe(0)
|
|
103
|
+
expect(`${result.stdout}${result.stderr}`).toMatch(/usage|options|pi coding agent/i)
|
|
104
|
+
})
|
|
105
|
+
|
|
78
106
|
it('passes what the harness found as paths, the enabled registry items with them, and its own extension last', async () => {
|
|
79
107
|
const root = rootWithProject()
|
|
80
108
|
writeFile(join(root, 'registry/skills/release/SKILL.md'), '# Release\n')
|
|
@@ -284,7 +312,7 @@ describe('the session socket', () => {
|
|
|
284
312
|
})
|
|
285
313
|
})
|
|
286
314
|
|
|
287
|
-
describe('
|
|
315
|
+
describe('xagent-wiki command', () => {
|
|
288
316
|
it('maps commands onto memory and Wiki calls', () => {
|
|
289
317
|
expect(parseCli(['search', 'deploy', 'steps', '--kind', 'page'])).toEqual({ kind: 'call', call: 'wiki.search', params: { query: 'deploy steps', kinds: ['page'] } })
|
|
290
318
|
expect(parseCli(['read', '/projects/app/arch'])).toEqual({ kind: 'call', call: 'wiki.read', params: { id: '/projects/app/arch' } })
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// skills, the session socket), then follows the process through its extension. It never owns
|
|
4
4
|
// the terminal: the application spawns the process from the spec returned here and reports when
|
|
5
5
|
// it exits.
|
|
6
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
7
7
|
import { dirname, join } from 'node:path'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
|
|
@@ -15,7 +15,7 @@ import { log } from '../core/bus'
|
|
|
15
15
|
import type { HarnessEnv } from '../core/env'
|
|
16
16
|
import { HarnessError, invalid } from '../core/errors'
|
|
17
17
|
import { newId, now } from '../core/fs'
|
|
18
|
-
import {
|
|
18
|
+
import { prependSearchPath, searchPath } from '../core/path'
|
|
19
19
|
import type { BrokerSession, ConnectedApps } from '../mcp/service'
|
|
20
20
|
import type { CallerContext } from '../memory/caller'
|
|
21
21
|
import { dispatch } from '../memory/dispatch'
|
|
@@ -33,6 +33,7 @@ import type { ToolSpec } from '../tools/types'
|
|
|
33
33
|
import { APPS_COMMAND, WIKI_COMMAND } from './cli'
|
|
34
34
|
import { SOCKET_ENV, TOKEN_ENV } from './client'
|
|
35
35
|
import { TerminalController, type PolicyReply } from './controller'
|
|
36
|
+
import { resolvePiCommand, terminalCommandFiles } from './launch'
|
|
36
37
|
import { SessionSocket, socketPath } from './socket'
|
|
37
38
|
|
|
38
39
|
/** The files a terminal session needs on disk, outside this process. */
|
|
@@ -102,7 +103,7 @@ export interface TerminalsOptions {
|
|
|
102
103
|
/** Lists plugins and applies a person's choices, for the session's `/plugins` command. */
|
|
103
104
|
pluginControl?: PluginControl
|
|
104
105
|
assets?: TerminalAssets
|
|
105
|
-
/** The `pi` executable. Defaults to the
|
|
106
|
+
/** The `pi` executable. Defaults to the PATH command on POSIX and the bundled CLI on Windows. */
|
|
106
107
|
program?: () => string | null
|
|
107
108
|
}
|
|
108
109
|
|
|
@@ -134,19 +135,27 @@ export class Terminals {
|
|
|
134
135
|
private ensureCommands(): string {
|
|
135
136
|
const bin = join(this.options.stateDir, 'run', 'bin')
|
|
136
137
|
mkdirSync(bin, { recursive: true })
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
const wrapper = join(bin, name)
|
|
140
|
-
const text = `#!/bin/sh\nexec ${quote(this.assets.node)} ${quote(this.assets.cli)} ${mode} "$@"\n`
|
|
141
|
-
let current: string | null = null
|
|
138
|
+
for (const name of ['agentx-wiki', 'agentx-mcp', 'agentx-wiki.cmd', 'agentx-mcp.cmd']) {
|
|
139
|
+
const path = join(bin, name)
|
|
142
140
|
try {
|
|
143
|
-
|
|
141
|
+
if (lstatSync(path).isFile()) unlinkSync(path)
|
|
144
142
|
} catch {
|
|
145
|
-
|
|
143
|
+
// These names belonged to the previous generated wrappers.
|
|
146
144
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
}
|
|
146
|
+
for (const [name, mode] of [[WIKI_COMMAND, 'wiki'], [APPS_COMMAND, 'mcp']] as const) {
|
|
147
|
+
for (const file of terminalCommandFiles(process.platform, this.assets.node, this.assets.cli, name, mode)) {
|
|
148
|
+
const path = join(bin, file.name)
|
|
149
|
+
let current: string | null = null
|
|
150
|
+
try {
|
|
151
|
+
current = readFileSync(path, 'utf8')
|
|
152
|
+
} catch {
|
|
153
|
+
current = null
|
|
154
|
+
}
|
|
155
|
+
if (current !== file.content) {
|
|
156
|
+
writeFileSync(path, file.content)
|
|
157
|
+
chmodSync(path, 0o755)
|
|
158
|
+
}
|
|
150
159
|
}
|
|
151
160
|
}
|
|
152
161
|
return bin
|
|
@@ -298,8 +307,7 @@ export class Terminals {
|
|
|
298
307
|
const { root } = env
|
|
299
308
|
if (!input?.sessionId || !input.cwd || !input.sessionFolder) throw invalid('A terminal session needs an id, a working folder, and its own folder.')
|
|
300
309
|
await this.dispose(input.sessionId)
|
|
301
|
-
const
|
|
302
|
-
if (!program) throw new HarnessError('harness_unavailable', 'The Pi CLI was not found on PATH.')
|
|
310
|
+
const command = resolvePiCommand({ platform: process.platform, node: this.assets.node, programOverride: this.options.program })
|
|
303
311
|
// A pinned account is used as it is; it is never exchanged for another one.
|
|
304
312
|
const account = input.accountId ? accounts.find(input.accountId) : accounts.resolve(null, null)
|
|
305
313
|
const launch = launchEnv(accounts.dir, account)
|
|
@@ -343,7 +351,7 @@ export class Terminals {
|
|
|
343
351
|
live.token = token
|
|
344
352
|
this.sessions.set(sessionId, live)
|
|
345
353
|
|
|
346
|
-
const args = ['--append-system-prompt', render(env, pack)]
|
|
354
|
+
const args = [...command.argsPrefix, '--append-system-prompt', render(env, pack)]
|
|
347
355
|
if (input.model) args.push('--model', input.model)
|
|
348
356
|
if (loading) {
|
|
349
357
|
// Pi's own discovery is off: the terminal loads what the harness found and nothing else.
|
|
@@ -358,9 +366,15 @@ export class Terminals {
|
|
|
358
366
|
args.push('--extension', this.assets.piExtension, '--session-dir', this.historyDir(input))
|
|
359
367
|
if (input.nativeSessionId) args.push('--session', input.nativeSessionId)
|
|
360
368
|
return {
|
|
361
|
-
program,
|
|
369
|
+
program: command.program,
|
|
362
370
|
args,
|
|
363
|
-
env: {
|
|
371
|
+
env: {
|
|
372
|
+
...launch,
|
|
373
|
+
...(process.platform === 'win32' ? { XINGULAR_HARNESS_NODE: this.assets.node, XINGULAR_HARNESS_CLI: this.assets.cli } : {}),
|
|
374
|
+
...prependSearchPath(this.ensureCommands(), searchPath()),
|
|
375
|
+
[SOCKET_ENV]: this.socket.path,
|
|
376
|
+
[TOKEN_ENV]: token
|
|
377
|
+
},
|
|
364
378
|
accountId: account.id,
|
|
365
379
|
nativeSessionId: input.nativeSessionId ?? null
|
|
366
380
|
}
|
package/src/tools/definitions.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The memory and Wiki tools a session gets, defined once. An SDK session gets them as Pi tools;
|
|
2
|
-
// a terminal session gets them from the `
|
|
2
|
+
// a terminal session gets them from the `xagent-wiki` command or the bundled MCP server. All of
|
|
3
3
|
// them reach the same calls.
|
|
4
4
|
|
|
5
5
|
export type WikiCall =
|
|
@@ -116,7 +116,7 @@ export const WIKI_TOOLS: WikiToolDefinition[] = [
|
|
|
116
116
|
name: 'wiki_read',
|
|
117
117
|
call: 'wiki.read',
|
|
118
118
|
label: 'Read Wiki entry',
|
|
119
|
-
description: 'Read one Wiki page, fact, skill, rule, or prompt in full, with its sources, links, and backlinks. Pass an id (arch, skills/review) or a Wiki path (/projects/
|
|
119
|
+
description: 'Read one Wiki page, fact, skill, rule, or prompt in full, with its sources, links, and backlinks. Pass an id (arch, skills/review) or a Wiki path (/projects/xagent/arch).',
|
|
120
120
|
parameters: { type: 'object', properties: { id: text('Entry id or Wiki path'), revision: text('Optional pinned revision selected for this reply'), offset: { type: 'integer', minimum: 0, description: 'Character offset when reading a pinned revision; use nextOffset to continue.' } }, required: ['id'] }
|
|
121
121
|
},
|
|
122
122
|
{
|
package/src/tools/types.ts
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
import type { CallerContext } from '../memory/caller'
|
|
4
4
|
import type { CapabilitySet } from '../capabilities/types'
|
|
5
5
|
import type { ActiveRun } from '../sessions/registry'
|
|
6
|
+
import type { ToolAttachmentRef } from '../sessions/contracts'
|
|
6
7
|
|
|
7
8
|
export interface ToolResult {
|
|
8
9
|
text: string
|
|
9
10
|
image?: { data: string; mimeType: string }
|
|
11
|
+
/** References to content stored by the provider; bytes never enter the run-event stream. */
|
|
12
|
+
attachments?: ToolAttachmentRef[]
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export interface ToolSpec {
|