@namzu/sdk 57.1.0 → 58.1.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 +72 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +2 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/file-definitions.d.ts +35 -4
- package/dist/agents/file-definitions.d.ts.map +1 -1
- package/dist/agents/file-definitions.js +98 -8
- package/dist/agents/file-definitions.js.map +1 -1
- package/dist/authorization/protected-paths.d.ts +9 -0
- package/dist/authorization/protected-paths.d.ts.map +1 -1
- package/dist/authorization/protected-paths.js +29 -3
- package/dist/authorization/protected-paths.js.map +1 -1
- package/dist/compaction/context-window.d.ts +1 -1
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +8 -1
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/compaction/plan.d.ts.map +1 -1
- package/dist/compaction/plan.js +2 -2
- package/dist/compaction/plan.js.map +1 -1
- package/dist/config/runtime.d.ts +10 -0
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +2 -0
- package/dist/config/runtime.js.map +1 -1
- package/dist/connector/mcp/client.d.ts +8 -0
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +75 -2
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/constants/mcp/index.d.ts +2 -0
- package/dist/constants/mcp/index.d.ts.map +1 -1
- package/dist/constants/mcp/index.js +2 -0
- package/dist/constants/mcp/index.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +2 -0
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +2 -0
- package/dist/constants/plugin/index.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +25 -4
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +2 -1
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/persona/assembler.d.ts.map +1 -1
- package/dist/persona/assembler.js +2 -2
- package/dist/persona/assembler.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/lifecycle.d.ts +8 -1
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +9 -2
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/shell-hook-http.d.ts +39 -0
- package/dist/plugin/shell-hook-http.d.ts.map +1 -0
- package/dist/plugin/shell-hook-http.js +163 -0
- package/dist/plugin/shell-hook-http.js.map +1 -0
- package/dist/plugin/shell-hook.d.ts +93 -11
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +223 -39
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/public-runtime.d.ts +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +1 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +2 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +1 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/query/executor.d.ts +1 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +34 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +2 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +25 -1
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +47 -10
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
- package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +7 -5
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts +2 -0
- package/dist/runtime/query/path-escalation.d.ts.map +1 -1
- package/dist/runtime/query/path-escalation.js +7 -4
- package/dist/runtime/query/path-escalation.js.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +2 -2
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/prepare-turn.d.ts +2 -0
- package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
- package/dist/runtime/query/prepare-turn.js +25 -0
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +3 -2
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +32 -13
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/session/retention/archive.js +1 -1
- package/dist/session/retention/archive.js.map +1 -1
- package/dist/session/workspace/driver.d.ts +6 -0
- package/dist/session/workspace/driver.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.js +28 -0
- package/dist/session/workspace/git-worktree.js.map +1 -1
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/loader.d.ts +7 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +53 -0
- package/dist/skills/loader.js.map +1 -1
- package/dist/skills/registry.d.ts +1 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +2 -1
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/builtins/bash-cwd.d.ts +52 -0
- package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
- package/dist/tools/builtins/bash-cwd.js +127 -0
- package/dist/tools/builtins/bash-cwd.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +41 -6
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
- package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
- package/dist/tools/builtins/bounded-file-read.js +67 -0
- package/dist/tools/builtins/bounded-file-read.js.map +1 -0
- package/dist/tools/builtins/grep.d.ts +6 -1
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +187 -24
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +113 -28
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-media.d.ts +68 -0
- package/dist/tools/builtins/read-media.d.ts.map +1 -0
- package/dist/tools/builtins/read-media.js +397 -0
- package/dist/tools/builtins/read-media.js.map +1 -0
- package/dist/tools/builtins/search-tools.js +1 -1
- package/dist/tools/builtins/search-tools.js.map +1 -1
- package/dist/tools/builtins/skill.d.ts +4 -2
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +61 -2
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/builtins/sleep-guard.d.ts +15 -0
- package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
- package/dist/tools/builtins/sleep-guard.js +33 -0
- package/dist/tools/builtins/sleep-guard.js.map +1 -0
- package/dist/tools/builtins/view-image.d.ts +23 -1
- package/dist/tools/builtins/view-image.d.ts.map +1 -1
- package/dist/tools/builtins/view-image.js +66 -58
- package/dist/tools/builtins/view-image.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +31 -0
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/toolsets/manager.js +3 -3
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +6 -2
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/agent/task.js.map +1 -1
- package/dist/types/connector/mcp.d.ts +46 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +1 -1
- package/dist/types/plugin/index.d.ts +72 -5
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +12 -1
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/session/sub-session.d.ts +6 -2
- package/dist/types/session/sub-session.d.ts.map +1 -1
- package/dist/types/skills/index.d.ts +36 -0
- package/dist/types/skills/index.d.ts.map +1 -1
- package/dist/types/skills/index.js +9 -0
- package/dist/types/skills/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +22 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/workspace/ref.d.ts +2 -0
- package/dist/types/workspace/ref.d.ts.map +1 -1
- package/dist/utils/frontmatter.d.ts +12 -0
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +6 -0
- package/dist/utils/frontmatter.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +2 -0
- package/src/agents/file-definitions.ts +126 -6
- package/src/authorization/protected-paths.ts +30 -2
- package/src/compaction/context-window.ts +10 -0
- package/src/compaction/plan.ts +12 -2
- package/src/config/runtime.ts +2 -0
- package/src/connector/mcp/client.ts +87 -1
- package/src/constants/mcp/index.ts +3 -0
- package/src/constants/plugin/index.ts +2 -0
- package/src/manager/agent/lifecycle.ts +24 -5
- package/src/manager/session/turn-recorder.ts +2 -1
- package/src/persona/assembler.ts +2 -2
- package/src/plugin/index.ts +5 -0
- package/src/plugin/lifecycle.ts +16 -2
- package/src/plugin/shell-hook-http.ts +181 -0
- package/src/plugin/shell-hook.ts +386 -89
- package/src/public-runtime.ts +2 -0
- package/src/public-tools.ts +2 -0
- package/src/public-types.ts +5 -0
- package/src/runtime/query/executor.ts +40 -10
- package/src/runtime/query/index.ts +33 -0
- package/src/runtime/query/iteration/index.ts +51 -11
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
- package/src/runtime/query/iteration/phases/compaction.ts +27 -5
- package/src/runtime/query/iteration/phases/context.ts +2 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
- package/src/runtime/query/path-escalation.ts +11 -5
- package/src/runtime/query/plugin-hooks.ts +2 -2
- package/src/runtime/query/prepare-turn.ts +33 -0
- package/src/runtime/query/review-policy.ts +45 -16
- package/src/session/retention/archive.ts +1 -1
- package/src/session/workspace/driver.ts +6 -0
- package/src/session/workspace/git-worktree.ts +31 -0
- package/src/skills/index.ts +6 -1
- package/src/skills/loader.ts +54 -0
- package/src/skills/registry.ts +7 -6
- package/src/tools/builtins/bash-cwd.ts +156 -0
- package/src/tools/builtins/bash.ts +79 -34
- package/src/tools/builtins/bounded-file-read.ts +80 -0
- package/src/tools/builtins/grep.ts +198 -28
- package/src/tools/builtins/read-file.ts +151 -30
- package/src/tools/builtins/read-media.ts +483 -0
- package/src/tools/builtins/search-tools.ts +1 -1
- package/src/tools/builtins/skill.ts +91 -2
- package/src/tools/builtins/sleep-guard.ts +31 -0
- package/src/tools/builtins/view-image.ts +74 -62
- package/src/tools/file-read-tracker.ts +30 -0
- package/src/toolsets/manager.ts +3 -3
- package/src/types/agent/query.ts +2 -0
- package/src/types/agent/task.ts +6 -2
- package/src/types/connector/mcp.ts +48 -0
- package/src/types/hitl/index.ts +1 -1
- package/src/types/plugin/index.ts +102 -16
- package/src/types/session/sub-session.ts +6 -2
- package/src/types/skills/index.ts +48 -0
- package/src/types/tool/index.ts +22 -0
- package/src/types/workspace/ref.ts +2 -0
- package/src/utils/frontmatter.ts +21 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { mkdtemp, readdir, rm } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { readBoundedFile } from './bounded-file-read.js'
|
|
6
|
+
import { screenshotTargetSize } from './computer-use-image.js'
|
|
7
|
+
import { describeRaster } from './view-image.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What `read` hands back for the files that are not plain text: the pages of a
|
|
11
|
+
* PDF as pictures and the cells of a notebook with what they printed. Each
|
|
12
|
+
* function returns text and pictures for the model, or a plain failure message.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Most pages one read may render. */
|
|
16
|
+
export const PDF_MAX_PAGES_PER_READ = 20
|
|
17
|
+
/** A PDF with more pages than this is not read whole; the model names pages. */
|
|
18
|
+
export const PDF_WHOLE_READ_MAX_PAGES = 10
|
|
19
|
+
/** The largest PDF read; larger files are refused before any tool runs. */
|
|
20
|
+
export const PDF_MAX_BYTES = 100 * 1024 * 1024
|
|
21
|
+
/** The largest notebook read in full. */
|
|
22
|
+
export const NOTEBOOK_MAX_BYTES = 8 * 1024 * 1024
|
|
23
|
+
/** Printed output of one cell above this many characters is left out. */
|
|
24
|
+
const CELL_OUTPUT_MAX_CHARS = 10_000
|
|
25
|
+
/** Encoded images plus UTF-8 text, not decoded image bytes. */
|
|
26
|
+
export const MEDIA_MAX_OUTPUT_BYTES = 24 * 1024 * 1024
|
|
27
|
+
export const NOTEBOOK_MAX_OUTPUT_BYTES = 1024 * 1024
|
|
28
|
+
const PAGE_MAX_BYTES = 16 * 1024 * 1024
|
|
29
|
+
const PDF_TOOL_TIMEOUT_MS = 120_000
|
|
30
|
+
const PDF_INFO_TIMEOUT_MS = 10_000
|
|
31
|
+
const RENDER_DPI = '100'
|
|
32
|
+
|
|
33
|
+
export interface MediaImage {
|
|
34
|
+
readonly mediaType: 'image/png' | 'image/jpeg' | 'image/webp'
|
|
35
|
+
/** Base64 of the image bytes. */
|
|
36
|
+
readonly data: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type MediaPart =
|
|
40
|
+
| { readonly type: 'text'; readonly text: string }
|
|
41
|
+
| ({ readonly type: 'image' } & MediaImage)
|
|
42
|
+
|
|
43
|
+
export type MediaRead =
|
|
44
|
+
| { readonly ok: true; readonly parts: readonly MediaPart[]; readonly facts: MediaFacts }
|
|
45
|
+
| { readonly ok: false; readonly message: string }
|
|
46
|
+
|
|
47
|
+
export interface MediaFacts {
|
|
48
|
+
readonly pages?: number
|
|
49
|
+
readonly totalPages?: number
|
|
50
|
+
readonly cells?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `"3"`, `"1-5"` and `"3-"`; anything else is not one range. */
|
|
54
|
+
export function parsePageRange(value: string): { first: number; last: number } | null {
|
|
55
|
+
const match = /^(\d{1,9})(?:\s*(-)\s*(\d{1,9})?)?$/.exec(value.trim())
|
|
56
|
+
if (!match) return null
|
|
57
|
+
const first = Number(match[1])
|
|
58
|
+
const last = match[3] ? Number(match[3]) : match[2] ? Number.POSITIVE_INFINITY : first
|
|
59
|
+
if (first < 1 || last < first) return null
|
|
60
|
+
return { first, last }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function checkPageRange(
|
|
64
|
+
value: string,
|
|
65
|
+
): { ok: true; first: number; last: number } | { ok: false; message: string } {
|
|
66
|
+
const range = parsePageRange(value)
|
|
67
|
+
if (!range) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
message: `Invalid pages parameter: "${value}". Give one page ("3") or one range ("1-5"), not a list. To read several pages or ranges, read each one separately. Pages are 1-indexed.`,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const count =
|
|
74
|
+
range.last === Number.POSITIVE_INFINITY
|
|
75
|
+
? PDF_MAX_PAGES_PER_READ + 1
|
|
76
|
+
: range.last - range.first + 1
|
|
77
|
+
if (count > PDF_MAX_PAGES_PER_READ) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
message: `Page range "${value}" exceeds maximum of ${PDF_MAX_PAGES_PER_READ} pages per request. Please use a smaller range.`,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, first: range.first, last: range.last }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface RunResult {
|
|
87
|
+
readonly code: number | null
|
|
88
|
+
readonly stdout: string
|
|
89
|
+
readonly stderr: string
|
|
90
|
+
readonly missing: boolean
|
|
91
|
+
readonly timedOut: boolean
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function run(
|
|
95
|
+
command: string,
|
|
96
|
+
args: readonly string[],
|
|
97
|
+
input: Buffer | undefined,
|
|
98
|
+
signal: AbortSignal,
|
|
99
|
+
timeoutMs: number,
|
|
100
|
+
): Promise<RunResult> {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const child = spawn(command, [...args], { stdio: ['pipe', 'pipe', 'pipe'], signal })
|
|
103
|
+
let stdout = ''
|
|
104
|
+
let stderr = ''
|
|
105
|
+
let missing = false
|
|
106
|
+
let timedOut = false
|
|
107
|
+
const timer = setTimeout(() => {
|
|
108
|
+
timedOut = true
|
|
109
|
+
child.kill('SIGKILL')
|
|
110
|
+
}, timeoutMs)
|
|
111
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
112
|
+
if (stdout.length < 1_000_000)
|
|
113
|
+
stdout += chunk.subarray(0, 1_000_000 - stdout.length).toString('utf8')
|
|
114
|
+
})
|
|
115
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
116
|
+
if (stderr.length < 1_000_000)
|
|
117
|
+
stderr += chunk.subarray(0, 1_000_000 - stderr.length).toString('utf8')
|
|
118
|
+
})
|
|
119
|
+
child.on('error', (error: NodeJS.ErrnoException) => {
|
|
120
|
+
missing = error.code === 'ENOENT'
|
|
121
|
+
clearTimeout(timer)
|
|
122
|
+
resolve({ code: null, stdout, stderr: stderr || error.message, missing, timedOut })
|
|
123
|
+
})
|
|
124
|
+
child.on('close', (code) => {
|
|
125
|
+
clearTimeout(timer)
|
|
126
|
+
resolve({ code, stdout, stderr, missing, timedOut })
|
|
127
|
+
})
|
|
128
|
+
// A tool that exits early closes its stdin; that is its answer, not ours.
|
|
129
|
+
child.stdin.on('error', () => {})
|
|
130
|
+
child.stdin.end(input)
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const PDF_TOOLS_MISSING =
|
|
135
|
+
'Page rendering needs pdftoppm, which is not installed. Install poppler-utils (for example `brew install poppler` or `apt-get install poppler-utils`), or extract the text with another tool.'
|
|
136
|
+
|
|
137
|
+
/** The page count, or null when it cannot be read (no pdfinfo, damaged file). */
|
|
138
|
+
async function pdfPageCount(bytes: Buffer, signal: AbortSignal): Promise<number | null> {
|
|
139
|
+
const result = await run('pdfinfo', ['-'], bytes, signal, PDF_INFO_TIMEOUT_MS)
|
|
140
|
+
if (result.code !== 0) return null
|
|
141
|
+
const match = /^Pages:\s+(\d+)/m.exec(result.stdout)
|
|
142
|
+
return match ? Number(match[1]) : null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function pdfFailure(stderr: string): string {
|
|
146
|
+
if (/password/i.test(stderr))
|
|
147
|
+
return 'PDF is password-protected. Please provide an unprotected version.'
|
|
148
|
+
if (/damaged|corrupt|invalid|Couldn't (?:find trailer dictionary|read xref table)/i.test(stderr))
|
|
149
|
+
return 'PDF file is corrupted or invalid.'
|
|
150
|
+
const first = stderr.split(/\r?\n/).find((line) => line.trim() !== '')
|
|
151
|
+
return `Could not render PDF${first ? `: ${first.trim()}` : '.'}`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function renderPages(
|
|
155
|
+
bytes: Buffer,
|
|
156
|
+
first: number,
|
|
157
|
+
last: number | undefined,
|
|
158
|
+
directory: string,
|
|
159
|
+
signal: AbortSignal,
|
|
160
|
+
): Promise<{ ok: true; names: string[] } | { ok: false; message: string; pageCount?: number }> {
|
|
161
|
+
const args = ['-jpeg', '-r', RENDER_DPI, '-scale-to', '4096', '-f', String(first)]
|
|
162
|
+
if (last !== undefined && Number.isFinite(last)) args.push('-l', String(last))
|
|
163
|
+
args.push('-', join(directory, 'page'))
|
|
164
|
+
const result = await run('pdftoppm', args, bytes, signal, PDF_TOOL_TIMEOUT_MS)
|
|
165
|
+
if (result.missing) return { ok: false, message: PDF_TOOLS_MISSING }
|
|
166
|
+
if (result.timedOut)
|
|
167
|
+
return { ok: false, message: 'Rendering the PDF took too long and was stopped.' }
|
|
168
|
+
if (result.code !== 0) {
|
|
169
|
+
const outside = /Wrong page range given.*last page \((\d+)\)/i.exec(result.stderr)
|
|
170
|
+
if (outside) {
|
|
171
|
+
const total = Number(outside[1])
|
|
172
|
+
if (total === 0)
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
message: 'PDF reports 0 pages (empty page tree). The PDF may be invalid.',
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
pageCount: total,
|
|
180
|
+
message: `The pages requested are outside the document (the PDF has ${total} ${total === 1 ? 'page' : 'pages'}). Use a range within 1-${total}, at most ${PDF_MAX_PAGES_PER_READ} pages per request (for example pages: "1-${Math.min(total, PDF_MAX_PAGES_PER_READ)}").`,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { ok: false, message: pdfFailure(result.stderr) }
|
|
184
|
+
}
|
|
185
|
+
const names = (await readdir(directory)).filter((name) => name.endsWith('.jpg')).sort()
|
|
186
|
+
if (names.length === 0)
|
|
187
|
+
return { ok: false, message: 'No pages were rendered. The PDF may be invalid.' }
|
|
188
|
+
return { ok: true, names }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Re-render one page at the size the image budget allows. */
|
|
192
|
+
async function renderPageFitted(
|
|
193
|
+
bytes: Buffer,
|
|
194
|
+
page: number,
|
|
195
|
+
width: number,
|
|
196
|
+
height: number,
|
|
197
|
+
directory: string,
|
|
198
|
+
signal: AbortSignal,
|
|
199
|
+
): Promise<Buffer | null> {
|
|
200
|
+
const base = join(directory, `fit-${page}`)
|
|
201
|
+
const result = await run(
|
|
202
|
+
'pdftoppm',
|
|
203
|
+
[
|
|
204
|
+
'-jpeg',
|
|
205
|
+
'-f',
|
|
206
|
+
String(page),
|
|
207
|
+
'-l',
|
|
208
|
+
String(page),
|
|
209
|
+
'-scale-to-x',
|
|
210
|
+
String(width),
|
|
211
|
+
'-scale-to-y',
|
|
212
|
+
String(height),
|
|
213
|
+
'-singlefile',
|
|
214
|
+
'-',
|
|
215
|
+
base,
|
|
216
|
+
],
|
|
217
|
+
bytes,
|
|
218
|
+
signal,
|
|
219
|
+
PDF_TOOL_TIMEOUT_MS,
|
|
220
|
+
)
|
|
221
|
+
if (result.code !== 0) return null
|
|
222
|
+
return readBoundedFile(`${base}.jpg`, PAGE_MAX_BYTES, signal).catch(() => null)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Render pages of a PDF as pictures. With `pages` it is exactly that range
|
|
227
|
+
* (at most 20); without it the whole file when it has at most 10 pages, and a
|
|
228
|
+
* message naming the `pages` parameter when it has more.
|
|
229
|
+
*/
|
|
230
|
+
export async function readPdfPages(
|
|
231
|
+
bytes: Buffer,
|
|
232
|
+
pagesArgument: string | undefined,
|
|
233
|
+
label: string,
|
|
234
|
+
signal: AbortSignal,
|
|
235
|
+
): Promise<MediaRead> {
|
|
236
|
+
if (bytes.length > PDF_MAX_BYTES)
|
|
237
|
+
return { ok: false, message: 'PDF exceeds the 100 MiB input limit.' }
|
|
238
|
+
let first = 1
|
|
239
|
+
let last: number | undefined
|
|
240
|
+
let total: number | null = null
|
|
241
|
+
if (pagesArgument !== undefined) {
|
|
242
|
+
const checked = checkPageRange(pagesArgument)
|
|
243
|
+
if (!checked.ok) return { ok: false, message: checked.message }
|
|
244
|
+
first = checked.first
|
|
245
|
+
last = checked.last
|
|
246
|
+
total = await pdfPageCount(bytes, signal)
|
|
247
|
+
} else {
|
|
248
|
+
total = await pdfPageCount(bytes, signal)
|
|
249
|
+
if (total !== null && total > PDF_WHOLE_READ_MAX_PAGES) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
message: `This PDF has ${total} pages, which is too many to read at once. Use the pages parameter to read specific page ranges (e.g., pages: "1-5"). Maximum ${PDF_MAX_PAGES_PER_READ} pages per request.`,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
last = total === null ? PDF_WHOLE_READ_MAX_PAGES : total
|
|
256
|
+
}
|
|
257
|
+
const directory = await mkdtemp(join(tmpdir(), 'namzu-pdf-'))
|
|
258
|
+
try {
|
|
259
|
+
const rendered = await renderPages(bytes, first, last, directory, signal)
|
|
260
|
+
if (!rendered.ok) return { ok: false, message: rendered.message }
|
|
261
|
+
const parts: MediaPart[] = []
|
|
262
|
+
let encodedBytes = 0
|
|
263
|
+
const firstName = rendered.names[0] as string
|
|
264
|
+
const firstShown = Number(/-(\d+)\.jpg$/.exec(firstName)?.[1] ?? first)
|
|
265
|
+
const header = `PDF pages extracted: ${rendered.names.length} ${rendered.names.length === 1 ? 'page' : 'pages'} from ${label} (${formatSize(bytes.length)})`
|
|
266
|
+
const lastShown = firstShown + rendered.names.length - 1
|
|
267
|
+
const more =
|
|
268
|
+
total !== null && lastShown < total
|
|
269
|
+
? `. This PDF has ${total} pages. These are pages ${firstShown}-${lastShown}. Use the pages parameter to read other pages (e.g., pages: "${lastShown + 1}-${Math.min(lastShown + rendered.names.length, total)}").`
|
|
270
|
+
: total === null
|
|
271
|
+
? `. The page count could not be read; these are pages ${firstShown}-${lastShown}.`
|
|
272
|
+
: ''
|
|
273
|
+
if (Buffer.byteLength(header + more, 'utf8') > 64 * 1024)
|
|
274
|
+
return { ok: false, message: 'PDF label exceeds the output budget.' }
|
|
275
|
+
parts.push({ type: 'text', text: `${header}${more}` })
|
|
276
|
+
for (const [index, name] of rendered.names.entries()) {
|
|
277
|
+
const page = firstShown + index
|
|
278
|
+
const unusable = `[Page ${page} could not be processed as an image.]`
|
|
279
|
+
try {
|
|
280
|
+
let image = await readBoundedFile(join(directory, name), PAGE_MAX_BYTES, signal)
|
|
281
|
+
const raster = describeRaster(image)
|
|
282
|
+
const target = screenshotTargetSize(raster.width, raster.height)
|
|
283
|
+
if (target.width !== raster.width || target.height !== raster.height) {
|
|
284
|
+
const fitted = await renderPageFitted(
|
|
285
|
+
bytes,
|
|
286
|
+
page,
|
|
287
|
+
target.width,
|
|
288
|
+
target.height,
|
|
289
|
+
directory,
|
|
290
|
+
signal,
|
|
291
|
+
)
|
|
292
|
+
if (!fitted) {
|
|
293
|
+
parts.push({ type: 'text', text: unusable })
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
image = fitted
|
|
297
|
+
}
|
|
298
|
+
const size = 4 * Math.ceil(image.length / 3)
|
|
299
|
+
if (encodedBytes + size > MEDIA_MAX_OUTPUT_BYTES - 128 * 1024)
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
message: 'PDF pictures exceed the 24 MiB encoded output budget. Request fewer pages.',
|
|
303
|
+
}
|
|
304
|
+
encodedBytes += size
|
|
305
|
+
parts.push({ type: 'image', mediaType: 'image/jpeg', data: image.toString('base64') })
|
|
306
|
+
} catch {
|
|
307
|
+
parts.push({ type: 'text', text: unusable })
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
ok: true,
|
|
312
|
+
parts,
|
|
313
|
+
facts: { pages: rendered.names.length, ...(total !== null ? { totalPages: total } : {}) },
|
|
314
|
+
}
|
|
315
|
+
} finally {
|
|
316
|
+
await rm(directory, { recursive: true, force: true }).catch(() => {})
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formatSize(n: number): string {
|
|
321
|
+
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MiB`
|
|
322
|
+
if (n >= 1024) return `${Math.round(n / 1024)} KiB`
|
|
323
|
+
return `${n} B`
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const NOTEBOOK_INVALID =
|
|
327
|
+
'Notebook file is not a valid Jupyter notebook (top-level "cells" must be an array of cell objects, each with a string or string-array "source").'
|
|
328
|
+
|
|
329
|
+
interface NotebookCell {
|
|
330
|
+
readonly id?: unknown
|
|
331
|
+
readonly cell_type?: unknown
|
|
332
|
+
readonly source?: unknown
|
|
333
|
+
readonly execution_count?: unknown
|
|
334
|
+
readonly outputs?: unknown
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function joined(value: unknown): string | null {
|
|
338
|
+
if (typeof value === 'string') return value
|
|
339
|
+
if (Array.isArray(value) && value.every((line) => typeof line === 'string')) return value.join('')
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
344
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function outputParts(output: unknown):
|
|
348
|
+
| {
|
|
349
|
+
text: string
|
|
350
|
+
image: MediaImage | undefined
|
|
351
|
+
imageSize?: number
|
|
352
|
+
}
|
|
353
|
+
| undefined {
|
|
354
|
+
if (!isRecord(output)) return undefined
|
|
355
|
+
switch (output.output_type) {
|
|
356
|
+
case 'stream':
|
|
357
|
+
return { text: joined(output.text) ?? '', image: undefined }
|
|
358
|
+
case 'execute_result':
|
|
359
|
+
case 'display_data': {
|
|
360
|
+
const data = isRecord(output.data) ? output.data : {}
|
|
361
|
+
const rawImage = data['image/png'] ?? data['image/jpeg']
|
|
362
|
+
if (typeof rawImage === 'string' && rawImage.length > CELL_OUTPUT_MAX_CHARS)
|
|
363
|
+
return {
|
|
364
|
+
text: joined(data['text/plain']) ?? '',
|
|
365
|
+
image: undefined,
|
|
366
|
+
imageSize: rawImage.length,
|
|
367
|
+
}
|
|
368
|
+
return { text: joined(data['text/plain']) ?? '', image: notebookImage(data) }
|
|
369
|
+
}
|
|
370
|
+
case 'error': {
|
|
371
|
+
const trace = Array.isArray(output.traceback) ? output.traceback.join('\n') : ''
|
|
372
|
+
return {
|
|
373
|
+
text: `${String(output.ename ?? '')}: ${String(output.evalue ?? '')}\n${trace}`,
|
|
374
|
+
image: undefined,
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
default:
|
|
378
|
+
return undefined
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function notebookImage(data: Record<string, unknown>): MediaImage | undefined {
|
|
383
|
+
for (const mediaType of ['image/png', 'image/jpeg'] as const) {
|
|
384
|
+
const value = data[mediaType]
|
|
385
|
+
if (typeof value !== 'string') continue
|
|
386
|
+
const base64 = value.replace(/\s/g, '')
|
|
387
|
+
if (base64.length > NOTEBOOK_MAX_OUTPUT_BYTES || !/^[A-Za-z0-9+/]*={0,2}$/.test(base64))
|
|
388
|
+
return undefined
|
|
389
|
+
try {
|
|
390
|
+
const decoded = Buffer.from(base64, 'base64')
|
|
391
|
+
if (decoded.toString('base64') !== base64) return undefined
|
|
392
|
+
describeRaster(decoded)
|
|
393
|
+
} catch {
|
|
394
|
+
return undefined
|
|
395
|
+
}
|
|
396
|
+
return { mediaType, data: base64 }
|
|
397
|
+
}
|
|
398
|
+
return undefined
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* A notebook as text the model can work from: every cell as
|
|
403
|
+
* `<cell id="...">source</cell id="...">` (a non-code cell names its type, a
|
|
404
|
+
* code cell in a language other than Python names the language), each followed
|
|
405
|
+
* by what it printed. A cell whose output is over 10,000 characters keeps only a
|
|
406
|
+
* note on how to read it.
|
|
407
|
+
*/
|
|
408
|
+
export function readNotebook(bytes: Buffer, label: string): MediaRead {
|
|
409
|
+
if (bytes.length > NOTEBOOK_MAX_BYTES) {
|
|
410
|
+
return {
|
|
411
|
+
ok: false,
|
|
412
|
+
message: `Notebook file (${formatSize(bytes.length)}) exceeds the maximum size this tool can read (${formatSize(NOTEBOOK_MAX_BYTES)}). Use a shell command with jq to read specific portions, for example: jq '.cells[:20]' ${label}`,
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
let parsed: unknown
|
|
416
|
+
try {
|
|
417
|
+
parsed = JSON.parse(bytes.toString('utf-8'))
|
|
418
|
+
} catch (error) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
message: `Notebook file is not valid JSON (it may be truncated, corrupted, or still being written): ${error instanceof Error ? error.message : String(error)}`,
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.cells))
|
|
425
|
+
return { ok: false, message: NOTEBOOK_INVALID }
|
|
426
|
+
const cells = parsed.cells as unknown[]
|
|
427
|
+
if (!cells.every((cell) => isRecord(cell) && joined((cell as NotebookCell).source) !== null)) {
|
|
428
|
+
return { ok: false, message: NOTEBOOK_INVALID }
|
|
429
|
+
}
|
|
430
|
+
const metadata = isRecord(parsed.metadata) ? parsed.metadata : {}
|
|
431
|
+
const info = isRecord(metadata.language_info) ? metadata.language_info : {}
|
|
432
|
+
const language = typeof info.name === 'string' ? info.name : 'python'
|
|
433
|
+
const parts: MediaPart[] = []
|
|
434
|
+
let outputBytes = 0
|
|
435
|
+
let exceeded = false
|
|
436
|
+
const pushText = (text: string): void => {
|
|
437
|
+
outputBytes += Buffer.byteLength(text, 'utf8') + (parts.length > 0 ? 1 : 0)
|
|
438
|
+
if (outputBytes > NOTEBOOK_MAX_OUTPUT_BYTES) {
|
|
439
|
+
exceeded = true
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
parts.push({ type: 'text', text })
|
|
443
|
+
}
|
|
444
|
+
for (const [index, raw] of (cells as NotebookCell[]).entries()) {
|
|
445
|
+
const id = typeof raw.id === 'string' ? raw.id : `cell-${index}`
|
|
446
|
+
const type = typeof raw.cell_type === 'string' ? raw.cell_type : 'code'
|
|
447
|
+
const tags =
|
|
448
|
+
(type !== 'code' ? `<cell_type>${type}</cell_type>` : '') +
|
|
449
|
+
(type === 'code' && language !== 'python' ? `<language>${language}</language>` : '')
|
|
450
|
+
pushText(`<cell id="${id}">${tags}${joined(raw.source)}</cell id="${id}">`)
|
|
451
|
+
if (exceeded) break
|
|
452
|
+
if (type !== 'code' || !Array.isArray(raw.outputs) || raw.outputs.length === 0) continue
|
|
453
|
+
const outputs = raw.outputs.map(outputParts).filter((o) => o !== undefined)
|
|
454
|
+
const size = outputs.reduce(
|
|
455
|
+
(sum, o) => sum + o.text.length + (o.imageSize ?? o.image?.data.length ?? 0),
|
|
456
|
+
0,
|
|
457
|
+
)
|
|
458
|
+
if (size > CELL_OUTPUT_MAX_CHARS) {
|
|
459
|
+
pushText(
|
|
460
|
+
`Outputs are too large to include. Use a shell command with: jq '.cells[${index}].outputs' ${label}`,
|
|
461
|
+
)
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
for (const output of outputs) {
|
|
465
|
+
if (output.text) pushText(`\n${output.text}`)
|
|
466
|
+
if (output.image) {
|
|
467
|
+
outputBytes += output.image.data.length
|
|
468
|
+
if (outputBytes > NOTEBOOK_MAX_OUTPUT_BYTES) {
|
|
469
|
+
exceeded = true
|
|
470
|
+
break
|
|
471
|
+
}
|
|
472
|
+
parts.push({ type: 'image', ...output.image })
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (exceeded)
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
message:
|
|
480
|
+
'Notebook exceeds the 1 MiB encoded output budget. Extract selected cells with jq and read the extracted file.',
|
|
481
|
+
}
|
|
482
|
+
return { ok: true, parts, facts: { cells: cells.length } }
|
|
483
|
+
}
|
|
@@ -21,7 +21,7 @@ export const SearchToolsTool = defineTool({
|
|
|
21
21
|
name: 'search_tools',
|
|
22
22
|
description:
|
|
23
23
|
'Search and load available tools by name or capability. ' +
|
|
24
|
-
'
|
|
24
|
+
'Use this only when a needed deferred tool schema is absent from the current runtime tools parameter. If its schema is already visible, call the tool directly without searching again.',
|
|
25
25
|
inputSchema,
|
|
26
26
|
category: 'analysis',
|
|
27
27
|
permissions: [],
|
|
@@ -4,6 +4,7 @@ import { z } from 'zod'
|
|
|
4
4
|
import { parseAllowedTools } from '../../authorization/skill-grant.js'
|
|
5
5
|
import type { Sandbox } from '../../types/sandbox/index.js'
|
|
6
6
|
import { isInvocableBy, skillInvocation } from '../../types/skills/index.js'
|
|
7
|
+
import type { ToolContext } from '../../types/tool/index.js'
|
|
7
8
|
import { defineTool } from '../defineTool.js'
|
|
8
9
|
|
|
9
10
|
export { parseAllowedTools }
|
|
@@ -88,6 +89,13 @@ const inputSchema = z.object({
|
|
|
88
89
|
.describe(
|
|
89
90
|
'The skill to load, exactly as listed in the available-skills manifest. Omit it to list model-invocable skills.',
|
|
90
91
|
),
|
|
92
|
+
args: z
|
|
93
|
+
.string()
|
|
94
|
+
.max(8000)
|
|
95
|
+
.optional()
|
|
96
|
+
.describe(
|
|
97
|
+
'What to apply the skill to, in your own words. A skill that runs as a delegated agent sees only this and its own instructions, so say what it needs. Omit it on a continuation.',
|
|
98
|
+
),
|
|
91
99
|
cursor: z
|
|
92
100
|
.string()
|
|
93
101
|
.max(160)
|
|
@@ -306,6 +314,9 @@ function pageSkillBody(input: {
|
|
|
306
314
|
|
|
307
315
|
export const SKILL_TOOL_NAME = 'skill'
|
|
308
316
|
|
|
317
|
+
/** The tool a skill that sets `context: fork` is handed to. */
|
|
318
|
+
const DELEGATION_TOOL_NAME = 'Agent'
|
|
319
|
+
|
|
309
320
|
/**
|
|
310
321
|
* Build the `skill` tool, with what the host knows about where its model's
|
|
311
322
|
* tools run. {@link SkillTool} is this with no options.
|
|
@@ -471,6 +482,40 @@ export function createSkillTool(options: SkillToolOptions = {}) {
|
|
|
471
482
|
}
|
|
472
483
|
}
|
|
473
484
|
|
|
485
|
+
const argsLine =
|
|
486
|
+
input.args !== undefined && input.args.trim() !== ''
|
|
487
|
+
? `Arguments: ${input.args.trim()}\n\n`
|
|
488
|
+
: ''
|
|
489
|
+
|
|
490
|
+
// A skill that asks to run as a delegated agent is handed to the turn's
|
|
491
|
+
// delegation tool, with the agent type, model and effort it names. It
|
|
492
|
+
// is only attempted on a first call: a continuation belongs to an
|
|
493
|
+
// inline load. Without that tool this turn the skill loads inline, and
|
|
494
|
+
// says so, instead of failing.
|
|
495
|
+
let forkUnavailable = ''
|
|
496
|
+
if (skill.metadata.context === 'fork' && input.cursor === undefined) {
|
|
497
|
+
if (context.dispatchTool && context.toolRegistry?.has(DELEGATION_TOOL_NAME)) {
|
|
498
|
+
return runForkedSkill({
|
|
499
|
+
name: input.name,
|
|
500
|
+
skill,
|
|
501
|
+
argsLine,
|
|
502
|
+
dispatch: context.dispatchTool,
|
|
503
|
+
header: resolveModelDirectory
|
|
504
|
+
? directoryHeader(
|
|
505
|
+
nonEmpty(
|
|
506
|
+
await resolveModelDirectory(
|
|
507
|
+
{ name: input.name, directory: skill.dirPath },
|
|
508
|
+
directoryContextOf(context.sandbox),
|
|
509
|
+
),
|
|
510
|
+
),
|
|
511
|
+
)
|
|
512
|
+
: '',
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
forkUnavailable =
|
|
516
|
+
'[This skill is written to run as a delegated agent, but this turn cannot delegate. Follow its instructions yourself.]\n\n'
|
|
517
|
+
}
|
|
518
|
+
|
|
474
519
|
const allowed = parseAllowedTools(skill.metadata.allowedTools)
|
|
475
520
|
// Asked on every call, first page or continuation: the digest binds
|
|
476
521
|
// the answer, so a mount that changed between pages is a stale cursor
|
|
@@ -491,7 +536,7 @@ export function createSkillTool(options: SkillToolOptions = {}) {
|
|
|
491
536
|
// pattern is matched against a command line the model writes, and it
|
|
492
537
|
// writes the path it was told.
|
|
493
538
|
skillDirectory: resolveModelDirectory ? modelDirectory : skill.dirPath,
|
|
494
|
-
header: resolveModelDirectory ? directoryHeader(modelDirectory) : '',
|
|
539
|
+
header: forkUnavailable + (resolveModelDirectory ? directoryHeader(modelDirectory) : ''),
|
|
495
540
|
invocation,
|
|
496
541
|
}
|
|
497
542
|
const digest = snapshotDigest(snapshot)
|
|
@@ -528,7 +573,8 @@ export function createSkillTool(options: SkillToolOptions = {}) {
|
|
|
528
573
|
}
|
|
529
574
|
const notice = grantNotice(allowed, grant, context.grantSkillTools !== undefined)
|
|
530
575
|
const page = pageSkillBody({
|
|
531
|
-
snapshot
|
|
576
|
+
snapshot:
|
|
577
|
+
start === 0 && argsLine ? { ...snapshot, header: argsLine + snapshot.header } : snapshot,
|
|
532
578
|
digest,
|
|
533
579
|
start,
|
|
534
580
|
notice,
|
|
@@ -619,3 +665,46 @@ function grantNotice(
|
|
|
619
665
|
lines.push(unchanged)
|
|
620
666
|
return `\n\n[${lines.join(' ')}]`
|
|
621
667
|
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Run a `context: fork` skill through the turn's delegation tool.
|
|
671
|
+
*
|
|
672
|
+
* The delegated agent starts with none of the conversation, so its prompt is
|
|
673
|
+
* the skill's instructions plus the arguments the model gave. The skill's
|
|
674
|
+
* `agent`, `model`, `effort` and `background` pass straight through. The host's
|
|
675
|
+
* delegation tool decides whether effort without a model is supported. Never
|
|
676
|
+
* discard the request or invent a model to make it pass host validation.
|
|
677
|
+
*/
|
|
678
|
+
async function runForkedSkill(input: {
|
|
679
|
+
readonly name: string
|
|
680
|
+
readonly skill: {
|
|
681
|
+
readonly metadata: {
|
|
682
|
+
readonly agent?: string
|
|
683
|
+
readonly model?: string
|
|
684
|
+
readonly effort?: string
|
|
685
|
+
readonly background?: boolean
|
|
686
|
+
}
|
|
687
|
+
readonly body?: string
|
|
688
|
+
}
|
|
689
|
+
readonly argsLine: string
|
|
690
|
+
readonly header: string
|
|
691
|
+
readonly dispatch: NonNullable<ToolContext['dispatchTool']>
|
|
692
|
+
}) {
|
|
693
|
+
const { metadata, body } = input.skill
|
|
694
|
+
const result = await input.dispatch(DELEGATION_TOOL_NAME, {
|
|
695
|
+
description: `Skill ${input.name}`,
|
|
696
|
+
prompt: `${input.header}${input.argsLine}${body ?? '(this skill has no body)'}`,
|
|
697
|
+
...(metadata.agent === undefined ? {} : { subagent_type: metadata.agent }),
|
|
698
|
+
...(metadata.model === undefined ? {} : { model: metadata.model }),
|
|
699
|
+
...(metadata.effort === undefined ? {} : { effort: metadata.effort }),
|
|
700
|
+
...(metadata.background === undefined ? {} : { run_in_background: metadata.background }),
|
|
701
|
+
})
|
|
702
|
+
return {
|
|
703
|
+
...result,
|
|
704
|
+
data: {
|
|
705
|
+
...(typeof result.data === 'object' && result.data !== null ? result.data : {}),
|
|
706
|
+
skill: input.name,
|
|
707
|
+
forked: true,
|
|
708
|
+
},
|
|
709
|
+
}
|
|
710
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A leading `sleep` of this many seconds or more is refused where a better
|
|
3
|
+
* way to wait exists (a background job to wait on, or a monitor).
|
|
4
|
+
*/
|
|
5
|
+
export const LONG_SLEEP_SECONDS = 25
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Describes the long wait a command opens with, or `null` when it does not
|
|
9
|
+
* open with one. Only a first sub-command that is exactly `sleep <seconds>`
|
|
10
|
+
* counts; a sleep in the middle of a chain, or a suffixed one (`sleep 30s`),
|
|
11
|
+
* is left alone.
|
|
12
|
+
*/
|
|
13
|
+
export function leadingLongSleep(command: string): string | null {
|
|
14
|
+
const parts = command
|
|
15
|
+
.split(/&&|\|\||;|\||\n/)
|
|
16
|
+
.map((part) => part.trim())
|
|
17
|
+
.filter((part) => part !== '')
|
|
18
|
+
const head = parts[0]
|
|
19
|
+
if (head === undefined) return null
|
|
20
|
+
const match = /^sleep\s+(\d+(?:\.\d*)?)$/.exec(head)
|
|
21
|
+
if (!match) return null
|
|
22
|
+
const seconds = Number.parseFloat(match[1] ?? '')
|
|
23
|
+
if (!(seconds >= LONG_SLEEP_SECONDS)) return null
|
|
24
|
+
const rest = parts.slice(1).join(' ').trim()
|
|
25
|
+
return rest ? `sleep ${seconds} followed by: ${rest}` : `standalone sleep ${seconds}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What the model is told when a long leading sleep is refused. */
|
|
29
|
+
export function longSleepRefusal(what: string): string {
|
|
30
|
+
return `Blocked: ${what}. To wait for a condition, use the monitor tool with an until-loop (for example \`until <check>; do sleep 2; done\`). To wait for a command you started, use run_in_background: true and then wait_for_job. Do not chain shorter sleeps to work around this block.`
|
|
31
|
+
}
|