@namzu/sdk 48.3.1 → 49.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 +35 -0
- package/dist/config/registry.d.ts +2 -0
- package/dist/config/registry.d.ts.map +1 -1
- package/dist/config/registry.js +23 -3
- package/dist/config/registry.js.map +1 -1
- package/dist/connector/mcp/discovery.d.ts +9 -5
- package/dist/connector/mcp/discovery.d.ts.map +1 -1
- package/dist/connector/mcp/discovery.js +16 -8
- package/dist/connector/mcp/discovery.js.map +1 -1
- package/dist/connector/mcp/mcp-toolset.d.ts +3 -0
- package/dist/connector/mcp/mcp-toolset.d.ts.map +1 -1
- package/dist/connector/mcp/mcp-toolset.js +20 -2
- package/dist/connector/mcp/mcp-toolset.js.map +1 -1
- package/dist/connector/mcp/reconnect.d.ts +2 -0
- package/dist/connector/mcp/reconnect.d.ts.map +1 -1
- package/dist/connector/mcp/reconnect.js +36 -5
- package/dist/connector/mcp/reconnect.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts +14 -10
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +178 -41
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/prompt/resident-learning.js +2 -2
- package/dist/prompt/resident-learning.js.map +1 -1
- package/dist/prompt/resident-step.d.ts +2 -2
- package/dist/prompt/resident-step.js +4 -4
- package/dist/prompt/resident-step.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/registry/tool/screen.d.ts.map +1 -1
- package/dist/registry/tool/screen.js +33 -2
- package/dist/registry/tool/screen.js.map +1 -1
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +26 -5
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/index.d.ts +6 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +20 -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 +32 -8
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.js +2 -0
- package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +4 -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 +7 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.d.ts +2 -0
- package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.js +5 -1
- package/dist/runtime/query/iteration/step-shaping.js.map +1 -1
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/memory/disk.d.ts +7 -2
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +58 -5
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/index.d.ts +9 -0
- package/dist/store/memory/index.d.ts.map +1 -1
- package/dist/store/memory/index.js +43 -5
- package/dist/store/memory/index.js.map +1 -1
- package/dist/store/memory/markdown.d.ts +7 -2
- package/dist/store/memory/markdown.d.ts.map +1 -1
- package/dist/store/memory/markdown.js +33 -0
- package/dist/store/memory/markdown.js.map +1 -1
- package/dist/store/memory/memory.d.ts +7 -2
- package/dist/store/memory/memory.d.ts.map +1 -1
- package/dist/store/memory/memory.js +34 -0
- package/dist/store/memory/memory.js.map +1 -1
- package/dist/store/memory/revision.d.ts +16 -0
- package/dist/store/memory/revision.d.ts.map +1 -0
- package/dist/store/memory/revision.js +144 -0
- package/dist/store/memory/revision.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +6 -4
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job-bounds.js +42 -8
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts +1 -0
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job.js +24 -10
- package/dist/tools/builtins/wait-for-job.js.map +1 -1
- package/dist/tools/memory/delete.d.ts.map +1 -1
- package/dist/tools/memory/delete.js +38 -3
- package/dist/tools/memory/delete.js.map +1 -1
- package/dist/tools/memory/read.d.ts.map +1 -1
- package/dist/tools/memory/read.js +38 -2
- package/dist/tools/memory/read.js.map +1 -1
- package/dist/tools/memory/search.d.ts.map +1 -1
- package/dist/tools/memory/search.js +38 -7
- package/dist/tools/memory/search.js.map +1 -1
- package/dist/tools/memory/update.d.ts.map +1 -1
- package/dist/tools/memory/update.js +30 -3
- package/dist/tools/memory/update.js.map +1 -1
- package/dist/toolsets/manager.d.ts +13 -0
- package/dist/toolsets/manager.d.ts.map +1 -1
- package/dist/toolsets/manager.js +60 -1
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/turn/memory-recall.d.ts +2 -0
- package/dist/turn/memory-recall.d.ts.map +1 -1
- package/dist/turn/memory-recall.js +3 -1
- package/dist/turn/memory-recall.js.map +1 -1
- package/dist/types/memory/index.d.ts +29 -0
- package/dist/types/memory/index.d.ts.map +1 -1
- package/dist/types/memory/index.js +6 -0
- package/dist/types/memory/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +12 -2
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +5 -2
- package/dist/types/plugin/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config/registry.ts +24 -3
- package/src/connector/mcp/discovery.ts +29 -8
- package/src/connector/mcp/mcp-toolset.ts +25 -4
- package/src/connector/mcp/reconnect.ts +34 -4
- package/src/plugin/lifecycle.ts +217 -46
- package/src/prompt/resident-learning.ts +2 -2
- package/src/prompt/resident-step.ts +4 -4
- package/src/public-runtime.ts +2 -0
- package/src/registry/tool/screen.ts +34 -2
- package/src/runtime/jobs/registry.ts +25 -8
- package/src/runtime/query/index.ts +24 -1
- package/src/runtime/query/iteration/index.ts +35 -10
- package/src/runtime/query/iteration/outstanding-work.ts +2 -0
- package/src/runtime/query/iteration/phases/context.ts +4 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +6 -1
- package/src/runtime/query/iteration/step-shaping.ts +9 -1
- package/src/store/index.ts +1 -0
- package/src/store/memory/disk.ts +82 -7
- package/src/store/memory/index.ts +66 -7
- package/src/store/memory/markdown.ts +54 -2
- package/src/store/memory/memory.ts +52 -2
- package/src/store/memory/revision.ts +158 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +42 -7
- package/src/tools/builtins/wait-for-job.ts +25 -11
- package/src/tools/memory/delete.ts +33 -3
- package/src/tools/memory/read.ts +35 -2
- package/src/tools/memory/search.ts +38 -7
- package/src/tools/memory/update.ts +28 -3
- package/src/toolsets/manager.ts +74 -1
- package/src/turn/memory-recall.ts +5 -1
- package/src/types/memory/index.ts +43 -0
- package/src/types/plugin/index.ts +11 -6
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { MemoryId } from '../../types/ids/index.js'
|
|
2
2
|
import type {
|
|
3
|
+
ConditionalMemoryStore,
|
|
3
4
|
CreateMemoryParams,
|
|
4
5
|
MemoryContent,
|
|
5
6
|
MemoryIndexEntry,
|
|
6
7
|
MemoryRecord,
|
|
7
8
|
MemorySearchParams,
|
|
8
9
|
MemorySearchResult,
|
|
9
|
-
MemoryStore,
|
|
10
10
|
UpdateMemoryParams,
|
|
11
|
+
VersionedMemoryRecord,
|
|
11
12
|
} from '../../types/memory/index.js'
|
|
12
13
|
import { assertMemoryStatus } from '../../types/memory/index.js'
|
|
13
14
|
import { generateMemoryId } from '../../utils/id.js'
|
|
@@ -18,8 +19,9 @@ import {
|
|
|
18
19
|
nameHolder,
|
|
19
20
|
withOptionalFields,
|
|
20
21
|
} from './naming.js'
|
|
22
|
+
import { assertMemoryRevision, memoryRevision } from './revision.js'
|
|
21
23
|
|
|
22
|
-
export class InMemoryMemoryStore implements
|
|
24
|
+
export class InMemoryMemoryStore implements ConditionalMemoryStore {
|
|
23
25
|
private content = new Map<string, MemoryContent>()
|
|
24
26
|
private index = new InMemoryMemoryIndex()
|
|
25
27
|
|
|
@@ -70,10 +72,45 @@ export class InMemoryMemoryStore implements MemoryStore {
|
|
|
70
72
|
return entry && content ? structuredClone({ entry, content }) : undefined
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
async getVersionedRecord(id: MemoryId): Promise<VersionedMemoryRecord | undefined> {
|
|
76
|
+
const entry = this.index.getEntry(id)
|
|
77
|
+
const content = this.content.get(id)
|
|
78
|
+
if (!entry || !content) return undefined
|
|
79
|
+
const record = structuredClone({ entry, content })
|
|
80
|
+
return { ...record, revision: memoryRevision(record) }
|
|
81
|
+
}
|
|
82
|
+
|
|
73
83
|
async update(id: MemoryId, updates: UpdateMemoryParams): Promise<MemoryIndexEntry | undefined> {
|
|
84
|
+
return this.updateNow(id, updates)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async updateIfRevision(
|
|
88
|
+
id: MemoryId,
|
|
89
|
+
updates: UpdateMemoryParams,
|
|
90
|
+
expectedRevision: string,
|
|
91
|
+
): Promise<MemoryIndexEntry> {
|
|
92
|
+
// No await between the check and mutation: one in-memory operation turn.
|
|
93
|
+
const entry = this.updateNow(id, updates, expectedRevision)
|
|
94
|
+
if (!entry) throw new Error('A conditional memory update unexpectedly found no record.')
|
|
95
|
+
return entry
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private updateNow(
|
|
99
|
+
id: MemoryId,
|
|
100
|
+
updates: UpdateMemoryParams,
|
|
101
|
+
expectedRevision?: string,
|
|
102
|
+
): MemoryIndexEntry | undefined {
|
|
74
103
|
if (updates.status !== undefined) assertMemoryStatus(updates.status)
|
|
75
104
|
assertOptionalMemoryFields(updates)
|
|
76
105
|
const existing = this.index.getEntry(id)
|
|
106
|
+
if (expectedRevision !== undefined) {
|
|
107
|
+
const content = this.content.get(id)
|
|
108
|
+
assertMemoryRevision(
|
|
109
|
+
id,
|
|
110
|
+
existing && content ? { entry: existing, content } : undefined,
|
|
111
|
+
expectedRevision,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
77
114
|
if (!existing) return undefined
|
|
78
115
|
if (updates.name !== undefined) {
|
|
79
116
|
const holder = nameHolder(this.index.allEntries(), updates.name, id)
|
|
@@ -118,6 +155,19 @@ export class InMemoryMemoryStore implements MemoryStore {
|
|
|
118
155
|
}
|
|
119
156
|
|
|
120
157
|
async delete(id: MemoryId): Promise<boolean> {
|
|
158
|
+
return this.deleteNow(id)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async deleteIfRevision(id: MemoryId, expectedRevision: string): Promise<void> {
|
|
162
|
+
this.deleteNow(id, expectedRevision)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private deleteNow(id: MemoryId, expectedRevision?: string): boolean {
|
|
166
|
+
if (expectedRevision !== undefined) {
|
|
167
|
+
const entry = this.index.getEntry(id)
|
|
168
|
+
const content = this.content.get(id)
|
|
169
|
+
assertMemoryRevision(id, entry && content ? { entry, content } : undefined, expectedRevision)
|
|
170
|
+
}
|
|
121
171
|
const existed = this.index.remove(id)
|
|
122
172
|
this.content.delete(id)
|
|
123
173
|
return existed
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { NamzuError } from '../../types/errors/index.js'
|
|
3
|
+
import type { MemoryId } from '../../types/ids/index.js'
|
|
4
|
+
import type { MemoryRecord } from '../../types/memory/index.js'
|
|
5
|
+
|
|
6
|
+
/** A record can be read, but its metadata cannot be fingerprinted synchronously. */
|
|
7
|
+
export class MemoryRevisionUnavailableError extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super('This memory contains metadata that cannot be revision-checked.')
|
|
10
|
+
this.name = 'MemoryRevisionUnavailableError'
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Encode the same structured-clone snapshot `getRecord` returns. Type tags,
|
|
16
|
+
* object references and sorted plain-object keys preserve values JSON loses:
|
|
17
|
+
* BigInt, undefined, Map/Set contents, Date, typed arrays and cycles. Map and
|
|
18
|
+
* Set order is kept because it is observable. Unknown host objects (notably
|
|
19
|
+
* Blob, whose bytes need asynchronous reading) fail closed rather than hash as
|
|
20
|
+
* `{}` and allow a stale conditional write.
|
|
21
|
+
*/
|
|
22
|
+
function canonicalRecord(record: MemoryRecord): string {
|
|
23
|
+
let snapshot: MemoryRecord
|
|
24
|
+
try {
|
|
25
|
+
snapshot = structuredClone(record)
|
|
26
|
+
} catch {
|
|
27
|
+
throw new MemoryRevisionUnavailableError()
|
|
28
|
+
}
|
|
29
|
+
const seen = new WeakMap<object, number>()
|
|
30
|
+
let nextId = 0
|
|
31
|
+
|
|
32
|
+
function encode(value: unknown): unknown {
|
|
33
|
+
if (value === null) return ['null']
|
|
34
|
+
switch (typeof value) {
|
|
35
|
+
case 'undefined':
|
|
36
|
+
return ['undefined']
|
|
37
|
+
case 'string':
|
|
38
|
+
return ['string', value]
|
|
39
|
+
case 'boolean':
|
|
40
|
+
return ['boolean', value]
|
|
41
|
+
case 'bigint':
|
|
42
|
+
return ['bigint', value.toString()]
|
|
43
|
+
case 'number':
|
|
44
|
+
return ['number', Object.is(value, -0) ? '-0' : String(value)]
|
|
45
|
+
case 'function':
|
|
46
|
+
case 'symbol':
|
|
47
|
+
throw new MemoryRevisionUnavailableError()
|
|
48
|
+
case 'object':
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const object = value as object
|
|
53
|
+
const previous = seen.get(object)
|
|
54
|
+
if (previous !== undefined) return ['ref', previous]
|
|
55
|
+
const id = nextId++
|
|
56
|
+
seen.set(object, id)
|
|
57
|
+
if (Array.isArray(object)) {
|
|
58
|
+
return [
|
|
59
|
+
'array',
|
|
60
|
+
id,
|
|
61
|
+
object.length,
|
|
62
|
+
Object.keys(object)
|
|
63
|
+
.sort()
|
|
64
|
+
.map((key) => [key, encode((object as unknown as Record<string, unknown>)[key])]),
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
if (object instanceof Map) {
|
|
68
|
+
return ['map', id, [...object].map(([key, item]) => [encode(key), encode(item)])]
|
|
69
|
+
}
|
|
70
|
+
if (object instanceof Set) return ['set', id, [...object].map(encode)]
|
|
71
|
+
if (object instanceof Date) return ['date', id, encode(object.getTime())]
|
|
72
|
+
if (object instanceof RegExp)
|
|
73
|
+
return ['regexp', id, object.source, object.flags, object.lastIndex]
|
|
74
|
+
if (object instanceof ArrayBuffer) {
|
|
75
|
+
const buffer = object as ArrayBuffer & {
|
|
76
|
+
readonly maxByteLength?: number
|
|
77
|
+
readonly resizable?: boolean
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
'array-buffer',
|
|
81
|
+
id,
|
|
82
|
+
buffer.byteLength,
|
|
83
|
+
buffer.maxByteLength,
|
|
84
|
+
buffer.resizable,
|
|
85
|
+
Buffer.from(buffer).toString('hex'),
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
if (ArrayBuffer.isView(object)) {
|
|
89
|
+
// A view exposes its entire backing buffer through `.buffer`, even
|
|
90
|
+
// when its visible slice is one byte. Structured cloning keeps that
|
|
91
|
+
// backing and preserves aliases between multiple views.
|
|
92
|
+
if (
|
|
93
|
+
(typeof SharedArrayBuffer !== 'undefined' && object.buffer instanceof SharedArrayBuffer) ||
|
|
94
|
+
(object.buffer instanceof ArrayBuffer &&
|
|
95
|
+
(object.buffer as ArrayBuffer & { readonly resizable?: boolean }).resizable === true)
|
|
96
|
+
)
|
|
97
|
+
throw new MemoryRevisionUnavailableError()
|
|
98
|
+
return [
|
|
99
|
+
'array-buffer-view',
|
|
100
|
+
id,
|
|
101
|
+
object.constructor.name,
|
|
102
|
+
object.byteOffset,
|
|
103
|
+
object.byteLength,
|
|
104
|
+
encode(object.buffer),
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
if (object instanceof Error) {
|
|
108
|
+
return ['error', id, object.name, object.message, object.stack, encode(object.cause)]
|
|
109
|
+
}
|
|
110
|
+
if (object instanceof Number || object instanceof String || object instanceof Boolean)
|
|
111
|
+
return ['boxed', id, object.constructor.name, encode(object.valueOf())]
|
|
112
|
+
if (typeof SharedArrayBuffer !== 'undefined' && object instanceof SharedArrayBuffer)
|
|
113
|
+
throw new MemoryRevisionUnavailableError()
|
|
114
|
+
const prototype = Object.getPrototypeOf(object)
|
|
115
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
116
|
+
throw new MemoryRevisionUnavailableError()
|
|
117
|
+
return [
|
|
118
|
+
'object',
|
|
119
|
+
id,
|
|
120
|
+
Object.keys(object as Record<string, unknown>)
|
|
121
|
+
.sort()
|
|
122
|
+
.map((key) => [key, encode((object as Record<string, unknown>)[key])]),
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return JSON.stringify(encode(snapshot))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A content-derived token; it includes metadata as well as the body. */
|
|
130
|
+
export function memoryRevision(record: MemoryRecord): string {
|
|
131
|
+
return `m2:${createHash('sha256').update(canonicalRecord(record)).digest('hex')}`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The record changed or disappeared after its revision was read. */
|
|
135
|
+
export class MemoryRevisionConflictError extends NamzuError {
|
|
136
|
+
readonly memoryId: MemoryId
|
|
137
|
+
|
|
138
|
+
constructor(memoryId: MemoryId) {
|
|
139
|
+
super({
|
|
140
|
+
code: 'storage_error',
|
|
141
|
+
message: `Memory ${memoryId} changed since it was read. Read it again before changing it.`,
|
|
142
|
+
details: { memoryId, reason: 'revision_conflict' },
|
|
143
|
+
retryable: false,
|
|
144
|
+
})
|
|
145
|
+
this.name = 'MemoryRevisionConflictError'
|
|
146
|
+
this.memoryId = memoryId
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function assertMemoryRevision(
|
|
151
|
+
id: MemoryId,
|
|
152
|
+
record: MemoryRecord | undefined,
|
|
153
|
+
expectedRevision: string,
|
|
154
|
+
): asserts record is MemoryRecord {
|
|
155
|
+
if (!record || memoryRevision(record) !== expectedRevision) {
|
|
156
|
+
throw new MemoryRevisionConflictError(id)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BackgroundJobRegistryRef } from '../../types/tool/index.js'
|
|
2
|
+
import { subscribeToAbort } from '../../utils/abort.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Waiting on a background job, bounded by two different questions — the
|
|
@@ -42,6 +43,8 @@ interface JobWaitProgress {
|
|
|
42
43
|
readonly output: string
|
|
43
44
|
readonly nextOffset: number
|
|
44
45
|
readonly droppedBytes: number
|
|
46
|
+
/** Earlier bytes omitted from this wait's own bounded output, separately from registry loss. */
|
|
47
|
+
readonly omittedOutputBytes: number
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export type JobWaitOutcome =
|
|
@@ -67,12 +70,27 @@ export type JobWaitOutcome =
|
|
|
67
70
|
*/
|
|
68
71
|
const POLL_INTERVAL_MS = 1_000
|
|
69
72
|
|
|
73
|
+
/** Leave room for the status and recovery instructions inside the tool-result budget. */
|
|
74
|
+
const MAX_WAIT_OUTPUT_BYTES = 32 * 1024
|
|
75
|
+
|
|
76
|
+
function boundedTail(value: string): { output: string; omittedBytes: number } {
|
|
77
|
+
const bytes = Buffer.from(value, 'utf8')
|
|
78
|
+
if (bytes.length <= MAX_WAIT_OUTPUT_BYTES) return { output: value, omittedBytes: 0 }
|
|
79
|
+
let start = bytes.length - MAX_WAIT_OUTPUT_BYTES
|
|
80
|
+
// The cap is in bytes, but the displayed tail must begin at a UTF-8 character.
|
|
81
|
+
while (start < bytes.length && ((bytes[start] ?? 0) & 0xc0) === 0x80) start++
|
|
82
|
+
return {
|
|
83
|
+
output: bytes.subarray(start).toString('utf8'),
|
|
84
|
+
omittedBytes: start,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
/**
|
|
71
|
-
* Await a job under both bounds,
|
|
89
|
+
* Await a job under both bounds, retaining a bounded tail as it goes.
|
|
72
90
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
91
|
+
* A completed or timed-out wait returns the bytes it observed up to its
|
|
92
|
+
* absolute cursor. If the displayed tail could not hold all of them, its
|
|
93
|
+
* omitted-byte count keeps that gap visible to the caller.
|
|
76
94
|
*/
|
|
77
95
|
export async function waitForJobWithBounds(
|
|
78
96
|
jobs: Pick<BackgroundJobRegistryRef, 'read' | 'waitForExit'>,
|
|
@@ -90,8 +108,16 @@ export async function waitForJobWithBounds(
|
|
|
90
108
|
let cursor = options.fromOffset ?? 0
|
|
91
109
|
let output = ''
|
|
92
110
|
let droppedBytes = 0
|
|
111
|
+
let omittedOutputBytes = 0
|
|
93
112
|
let settled = false
|
|
94
113
|
let poll: ReturnType<typeof setInterval> | undefined
|
|
114
|
+
// Each bounded wait owns its exit subscription. A timeout must release that
|
|
115
|
+
// subscription even when the job itself runs forever.
|
|
116
|
+
const waitAbort = new AbortController()
|
|
117
|
+
const forwardAbort = (): void => waitAbort.abort(options.signal?.reason)
|
|
118
|
+
let disposeCallerAbort = () => {}
|
|
119
|
+
if (options.signal?.aborted) forwardAbort()
|
|
120
|
+
else if (options.signal) disposeCallerAbort = subscribeToAbort(options.signal, forwardAbort)
|
|
95
121
|
|
|
96
122
|
/** Read whatever is new since `cursor`, and count it as progress if it is. */
|
|
97
123
|
const drain = (): void => {
|
|
@@ -99,11 +125,15 @@ export async function waitForJobWithBounds(
|
|
|
99
125
|
if (chunk.droppedBytes > 0) droppedBytes += chunk.droppedBytes
|
|
100
126
|
if (chunk.nextOffset > cursor) lastProgressAt = now()
|
|
101
127
|
cursor = chunk.nextOffset
|
|
102
|
-
if (chunk.chunk)
|
|
128
|
+
if (chunk.chunk) {
|
|
129
|
+
const bounded = boundedTail(output + chunk.chunk)
|
|
130
|
+
output = bounded.output
|
|
131
|
+
omittedOutputBytes += bounded.omittedBytes
|
|
132
|
+
}
|
|
103
133
|
}
|
|
104
134
|
|
|
105
135
|
try {
|
|
106
|
-
const exited = waitForExit(id, { signal:
|
|
136
|
+
const exited = waitForExit(id, { signal: waitAbort.signal }).then((job): JobWaitOutcome => {
|
|
107
137
|
// One last read: the job can exit between ticks, and the bytes
|
|
108
138
|
// it wrote in its final moment are exactly the ones a caller
|
|
109
139
|
// most wants — the error, the summary line, the exit trace.
|
|
@@ -115,6 +145,7 @@ export async function waitForJobWithBounds(
|
|
|
115
145
|
output,
|
|
116
146
|
nextOffset: cursor,
|
|
117
147
|
droppedBytes,
|
|
148
|
+
omittedOutputBytes,
|
|
118
149
|
}
|
|
119
150
|
})
|
|
120
151
|
|
|
@@ -136,6 +167,7 @@ export async function waitForJobWithBounds(
|
|
|
136
167
|
output,
|
|
137
168
|
nextOffset: cursor,
|
|
138
169
|
droppedBytes,
|
|
170
|
+
omittedOutputBytes,
|
|
139
171
|
})
|
|
140
172
|
return
|
|
141
173
|
}
|
|
@@ -150,6 +182,7 @@ export async function waitForJobWithBounds(
|
|
|
150
182
|
output,
|
|
151
183
|
nextOffset: cursor,
|
|
152
184
|
droppedBytes,
|
|
185
|
+
omittedOutputBytes,
|
|
153
186
|
})
|
|
154
187
|
}
|
|
155
188
|
}
|
|
@@ -165,6 +198,8 @@ export async function waitForJobWithBounds(
|
|
|
165
198
|
} finally {
|
|
166
199
|
settled = true
|
|
167
200
|
if (poll !== undefined) clearInterval(poll)
|
|
201
|
+
disposeCallerAbort()
|
|
202
|
+
waitAbort.abort(new Error('Job wait finished'))
|
|
168
203
|
}
|
|
169
204
|
}
|
|
170
205
|
|
|
@@ -174,7 +209,7 @@ export function describeJobWaitTimeout(
|
|
|
174
209
|
outcome: Extract<JobWaitOutcome, { kind: 'timeout' }>,
|
|
175
210
|
): string {
|
|
176
211
|
const seconds = Math.round(outcome.elapsedMs / 1000)
|
|
177
|
-
const resume = `call wait_for_job
|
|
212
|
+
const resume = `call wait_for_job or job read with from_offset ${outcome.nextOffset} to see only what comes next`
|
|
178
213
|
if (outcome.cause === 'idle') {
|
|
179
214
|
return `Job ${id} went quiet: no new output for a while, after ${seconds}s. It has not been stopped and may still be working — ${resume}.`
|
|
180
215
|
}
|
|
@@ -35,6 +35,12 @@ const MAX_WAIT_MS = readPositiveIntEnv('NAMZU_JOB_WAIT_MAX_MS', 60 * 60 * 1000)
|
|
|
35
35
|
|
|
36
36
|
const inputSchema = z.object({
|
|
37
37
|
id: z.string().describe('The job id, as returned by bash with run_in_background.'),
|
|
38
|
+
from_offset: z
|
|
39
|
+
.number()
|
|
40
|
+
.int()
|
|
41
|
+
.nonnegative()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Resume after a previous wait: pass its next_offset to receive only new output.'),
|
|
38
44
|
timeout_ms: z
|
|
39
45
|
.number()
|
|
40
46
|
.int()
|
|
@@ -60,7 +66,7 @@ type WaitForJobInput = z.infer<typeof inputSchema>
|
|
|
60
66
|
export const WaitForJobTool = defineTool({
|
|
61
67
|
name: 'wait_for_job',
|
|
62
68
|
description:
|
|
63
|
-
'Block until a background job started by bash with run_in_background ends, and return its
|
|
69
|
+
'Block until a background job started by bash with run_in_background ends, and return its bounded output in one call. Use this instead of calling job with action "read" in a loop: it costs one call and no waiting turns. A wait that runs too long or goes quiet returns its output and next_offset without stopping the job; pass that offset as from_offset on the next wait to avoid repeats.',
|
|
64
70
|
inputSchema,
|
|
65
71
|
category: 'shell',
|
|
66
72
|
permissions: ['shell_execute'],
|
|
@@ -128,6 +134,7 @@ export const WaitForJobTool = defineTool({
|
|
|
128
134
|
outcome = await waitForJobWithBounds(jobs, input.id, {
|
|
129
135
|
wallMs: input.timeout_ms ?? DEFAULT_TIMEOUT_MS,
|
|
130
136
|
idleMs: input.idle_timeout_ms ?? DEFAULT_IDLE_TIMEOUT_MS,
|
|
137
|
+
...(input.from_offset === undefined ? {} : { fromOffset: input.from_offset }),
|
|
131
138
|
signal: context.abortSignal,
|
|
132
139
|
})
|
|
133
140
|
} catch {
|
|
@@ -144,26 +151,32 @@ export const WaitForJobTool = defineTool({
|
|
|
144
151
|
}
|
|
145
152
|
}
|
|
146
153
|
|
|
154
|
+
const notices = [
|
|
155
|
+
...(outcome.droppedBytes > 0
|
|
156
|
+
? [`[${outcome.droppedBytes} bytes were dropped by the job's output retention cap]`]
|
|
157
|
+
: []),
|
|
158
|
+
...(outcome.omittedOutputBytes > 0
|
|
159
|
+
? [`[${outcome.omittedOutputBytes} earlier bytes omitted from this bounded wait result]`]
|
|
160
|
+
: []),
|
|
161
|
+
]
|
|
162
|
+
const observed = `${notices.length > 0 ? `${notices.join('\n')}\n` : ''}${outcome.output || '(no new output)'}`
|
|
163
|
+
|
|
147
164
|
if (outcome.kind === 'timeout') {
|
|
148
165
|
return {
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
// The wait observed a bound, not a failed command. A tool error
|
|
167
|
+
// would also be promoted as a durable failure by the turn's memory.
|
|
168
|
+
success: true,
|
|
169
|
+
output: `${describeJobWaitTimeout(input.id, outcome)}\n\n${observed}\n\n[next_offset ${outcome.nextOffset}]`,
|
|
151
170
|
data: {
|
|
152
171
|
jobId: input.id,
|
|
153
172
|
timedOut: outcome.cause,
|
|
154
173
|
nextOffset: outcome.nextOffset,
|
|
155
174
|
droppedBytes: outcome.droppedBytes,
|
|
175
|
+
omittedOutputBytes: outcome.omittedOutputBytes,
|
|
156
176
|
},
|
|
157
177
|
}
|
|
158
178
|
}
|
|
159
179
|
|
|
160
|
-
// The dropped count is stated, never absorbed — same rule `job read`
|
|
161
|
-
// follows, for the same reason: a job whose middle vanished quietly
|
|
162
|
-
// reads as a complete result that happens to be short.
|
|
163
|
-
const notice =
|
|
164
|
-
outcome.droppedBytes > 0
|
|
165
|
-
? `[${outcome.droppedBytes} bytes were dropped before this point — the job produced output faster than the retention cap holds]\n`
|
|
166
|
-
: ''
|
|
167
180
|
const status =
|
|
168
181
|
outcome.exitCode === undefined
|
|
169
182
|
? outcome.status
|
|
@@ -171,12 +184,13 @@ export const WaitForJobTool = defineTool({
|
|
|
171
184
|
|
|
172
185
|
return {
|
|
173
186
|
success: true,
|
|
174
|
-
output: `${
|
|
187
|
+
output: `${observed}\n\n[job ${input.id} is ${status}; next_offset ${outcome.nextOffset}]`,
|
|
175
188
|
data: {
|
|
176
189
|
jobId: input.id,
|
|
177
190
|
status: outcome.status,
|
|
178
191
|
nextOffset: outcome.nextOffset,
|
|
179
192
|
droppedBytes: outcome.droppedBytes,
|
|
193
|
+
omittedOutputBytes: outcome.omittedOutputBytes,
|
|
180
194
|
...(outcome.exitCode === undefined ? {} : { exitCode: outcome.exitCode }),
|
|
181
195
|
},
|
|
182
196
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
+
import { MemoryRevisionConflictError } from '../../store/memory/revision.js'
|
|
2
3
|
import type { MemoryStore } from '../../types/memory/index.js'
|
|
4
|
+
import { hasConditionalMemoryWrites } from '../../types/memory/index.js'
|
|
3
5
|
import type { ToolDefinition } from '../../types/tool/index.js'
|
|
4
6
|
import { asMemoryId } from '../../utils/id.js'
|
|
5
7
|
import { defineTool } from '../defineTool.js'
|
|
@@ -8,17 +10,45 @@ export function buildDeleteMemoryTool(store: MemoryStore): ToolDefinition {
|
|
|
8
10
|
return defineTool({
|
|
9
11
|
name: 'delete_memory',
|
|
10
12
|
description:
|
|
11
|
-
'Permanently delete a stored memory by ID. Use update_memory with status archived when the record should remain available for inspection. Deletion does not erase past conversation transcripts.',
|
|
13
|
+
'Permanently delete a stored memory by ID. Use update_memory with status archived when the record should remain available for inspection. Deletion does not erase past conversation transcripts. Pass the revision from read_memory when available to refuse a stale deletion.',
|
|
12
14
|
inputSchema: z.object({
|
|
13
15
|
id: z.string().describe('Memory ID to permanently delete'),
|
|
16
|
+
revision: z
|
|
17
|
+
.string()
|
|
18
|
+
.min(1)
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Opaque revision from read_memory; refuse deletion if the record changed'),
|
|
14
21
|
}),
|
|
15
22
|
category: 'custom',
|
|
16
23
|
permissions: [],
|
|
17
24
|
readOnly: false,
|
|
18
25
|
destructive: true,
|
|
19
26
|
concurrencySafe: true,
|
|
20
|
-
async execute({ id }) {
|
|
21
|
-
|
|
27
|
+
async execute({ id, revision }) {
|
|
28
|
+
if (revision !== undefined && !hasConditionalMemoryWrites(store)) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
output: 'This memory store does not support revision-checked deletion.',
|
|
32
|
+
error: 'Conditional memory deletion unavailable',
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
let removed: boolean
|
|
36
|
+
try {
|
|
37
|
+
if (revision === undefined) {
|
|
38
|
+
removed = await store.delete(asMemoryId(id))
|
|
39
|
+
} else if (hasConditionalMemoryWrites(store)) {
|
|
40
|
+
await store.deleteIfRevision(asMemoryId(id), revision)
|
|
41
|
+
removed = true
|
|
42
|
+
} else throw new Error('Conditional memory support changed during the deletion.')
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (!(error instanceof MemoryRevisionConflictError)) throw error
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
output: error.message,
|
|
48
|
+
error: 'Memory changed since read',
|
|
49
|
+
data: { reason: 'revision_conflict' },
|
|
50
|
+
}
|
|
51
|
+
}
|
|
22
52
|
return {
|
|
23
53
|
success: removed,
|
|
24
54
|
output: removed ? `Memory deleted: ${id}.` : `Memory ${id} not found.`,
|
package/src/tools/memory/read.ts
CHANGED
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
describeMemoryAge,
|
|
5
5
|
memoryLinkNames,
|
|
6
6
|
} from '../../store/memory/links.js'
|
|
7
|
+
import { MemoryRevisionUnavailableError } from '../../store/memory/revision.js'
|
|
7
8
|
import type { MemoryId } from '../../types/ids/index.js'
|
|
8
9
|
import type { MemoryContent, MemoryIndexEntry, MemoryStore } from '../../types/memory/index.js'
|
|
10
|
+
import { hasConditionalMemoryWrites } from '../../types/memory/index.js'
|
|
9
11
|
import type { ToolDefinition } from '../../types/tool/index.js'
|
|
10
12
|
import { defineTool } from '../defineTool.js'
|
|
11
13
|
import { allMemoryEntries, resolveMemoryReference } from './resolve.js'
|
|
@@ -43,7 +45,22 @@ export function buildReadMemoryTool(store: MemoryStore): ToolDefinition {
|
|
|
43
45
|
|
|
44
46
|
let entry: MemoryIndexEntry | undefined
|
|
45
47
|
let content: MemoryContent | undefined
|
|
46
|
-
|
|
48
|
+
let revision: string | undefined
|
|
49
|
+
if (hasConditionalMemoryWrites(store)) {
|
|
50
|
+
try {
|
|
51
|
+
const record = await store.getVersionedRecord(memoryId)
|
|
52
|
+
entry = record?.entry
|
|
53
|
+
content = record?.content
|
|
54
|
+
revision = record?.revision
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (!(error instanceof MemoryRevisionUnavailableError)) throw error
|
|
57
|
+
// Legacy in-memory records may hold cloneable host objects with
|
|
58
|
+
// inaccessible bytes. Read them without claiming CAS support.
|
|
59
|
+
const record = await store.getRecord?.(memoryId)
|
|
60
|
+
entry = record?.entry
|
|
61
|
+
content = record?.content ?? (await store.get(memoryId))
|
|
62
|
+
}
|
|
63
|
+
} else if (store.getRecord) {
|
|
47
64
|
const record = await store.getRecord(memoryId)
|
|
48
65
|
entry = record?.entry
|
|
49
66
|
content = record?.content
|
|
@@ -92,9 +109,24 @@ export function buildReadMemoryTool(store: MemoryStore): ToolDefinition {
|
|
|
92
109
|
// A JSON body stays exactly the stored text, parseable as it was
|
|
93
110
|
// before these notes existed; its age and links are in `data`.
|
|
94
111
|
const annotate = notes.length > 0 && content.format !== 'json'
|
|
112
|
+
const output = annotate ? `${content.content}\n\n---\n${notes.join('\n')}` : content.content
|
|
95
113
|
return {
|
|
96
114
|
success: true,
|
|
97
|
-
output
|
|
115
|
+
output,
|
|
116
|
+
// The executor sends output or content to the model, not data. Keep
|
|
117
|
+
// output byte-compatible (including JSON) while exposing the token
|
|
118
|
+
// in a separate model-visible text block.
|
|
119
|
+
...(revision !== undefined
|
|
120
|
+
? {
|
|
121
|
+
content: [
|
|
122
|
+
{ type: 'text' as const, text: output },
|
|
123
|
+
{
|
|
124
|
+
type: 'text' as const,
|
|
125
|
+
text: `Memory revision: ${revision}. Pass it as revision to update_memory or delete_memory to reject stale changes.`,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
}
|
|
129
|
+
: {}),
|
|
98
130
|
data: {
|
|
99
131
|
id: content.id,
|
|
100
132
|
format: content.format,
|
|
@@ -102,6 +134,7 @@ export function buildReadMemoryTool(store: MemoryStore): ToolDefinition {
|
|
|
102
134
|
...(entry?.name !== undefined ? { name: entry.name } : {}),
|
|
103
135
|
...(entry?.type !== undefined ? { type: entry.type } : {}),
|
|
104
136
|
...(entry ? { updatedAt: entry.updatedAt } : {}),
|
|
137
|
+
...(revision !== undefined ? { revision } : {}),
|
|
105
138
|
...(resolved.length > 0 ? { links: resolved } : {}),
|
|
106
139
|
},
|
|
107
140
|
}
|
|
@@ -10,12 +10,13 @@ import type { ToolDefinition } from '../../types/tool/index.js'
|
|
|
10
10
|
import { defineTool } from '../defineTool.js'
|
|
11
11
|
|
|
12
12
|
type SearchMemory = (params: MemorySearchParams) => MemorySearchResult | Promise<MemorySearchResult>
|
|
13
|
+
const MAX_MEMORY_SCAN = 256
|
|
13
14
|
|
|
14
15
|
function defineSearchMemoryTool(search: SearchMemory): ToolDefinition {
|
|
15
16
|
return defineTool({
|
|
16
17
|
name: 'search_memory',
|
|
17
18
|
description:
|
|
18
|
-
'Search active stored memories by relevant words or tags. The built-in
|
|
19
|
+
'Search active stored memories by relevant words or tags. The built-in stores rank matches in names, titles, descriptions, summaries and full content. Built-in search examines at most 256 recent candidates per call; if incomplete, continue with the returned scan_offset. Use read_memory for evidence. Set status to archived to inspect obsolete records.',
|
|
19
20
|
inputSchema: z.object({
|
|
20
21
|
query: z.string().optional().describe('Relevant words or identifiers to search'),
|
|
21
22
|
tags: z.array(z.string()).optional().describe('Filter by tags (all must match)'),
|
|
@@ -30,20 +31,47 @@ function defineSearchMemoryTool(search: SearchMemory): ToolDefinition {
|
|
|
30
31
|
.max(50)
|
|
31
32
|
.default(10)
|
|
32
33
|
.describe('Maximum results to return (1–50)'),
|
|
34
|
+
scan_offset: z
|
|
35
|
+
.number()
|
|
36
|
+
.int()
|
|
37
|
+
.min(0)
|
|
38
|
+
.default(0)
|
|
39
|
+
.describe('Candidate offset from an earlier incomplete search; starts at 0'),
|
|
33
40
|
}),
|
|
34
41
|
category: 'analysis',
|
|
35
42
|
permissions: [],
|
|
36
43
|
readOnly: true,
|
|
37
44
|
destructive: false,
|
|
38
45
|
concurrencySafe: true,
|
|
39
|
-
async execute({ query, tags, status, limit }) {
|
|
40
|
-
const result = await search({
|
|
46
|
+
async execute({ query, tags, status, limit, scan_offset }) {
|
|
47
|
+
const result = await search({
|
|
48
|
+
query,
|
|
49
|
+
tags,
|
|
50
|
+
status,
|
|
51
|
+
limit,
|
|
52
|
+
maxScanned: MAX_MEMORY_SCAN,
|
|
53
|
+
scanOffset: scan_offset,
|
|
54
|
+
})
|
|
55
|
+
const continuation = result.truncated
|
|
56
|
+
? result.nextScanOffset === undefined
|
|
57
|
+
? ' Search incomplete; narrow the query or read a known ID.'
|
|
58
|
+
: ` Search incomplete; continue with scan_offset ${result.nextScanOffset}.`
|
|
59
|
+
: ''
|
|
60
|
+
const page = scan_offset > 0 || result.truncated ? ' in this scan page' : ''
|
|
41
61
|
|
|
42
62
|
if (result.entries.length === 0) {
|
|
43
63
|
return {
|
|
44
64
|
success: true,
|
|
45
|
-
output:
|
|
46
|
-
data: {
|
|
65
|
+
output: `No memories found${page}.${continuation}`,
|
|
66
|
+
data: {
|
|
67
|
+
entries: [],
|
|
68
|
+
totalCount: result.totalCount,
|
|
69
|
+
truncated: result.truncated === true,
|
|
70
|
+
...(result.scannedCount !== undefined ? { scannedCount: result.scannedCount } : {}),
|
|
71
|
+
...(result.nextScanOffset !== undefined
|
|
72
|
+
? { nextScanOffset: result.nextScanOffset }
|
|
73
|
+
: {}),
|
|
74
|
+
},
|
|
47
75
|
}
|
|
48
76
|
}
|
|
49
77
|
|
|
@@ -57,8 +85,8 @@ function defineSearchMemoryTool(search: SearchMemory): ToolDefinition {
|
|
|
57
85
|
|
|
58
86
|
const output =
|
|
59
87
|
result.totalCount > result.entries.length
|
|
60
|
-
? `Found ${result.totalCount} memories (showing ${result.entries.length}):\n${lines.join('\n')}`
|
|
61
|
-
: `Found ${result.totalCount} memories:\n${lines.join('\n')}`
|
|
88
|
+
? `Found ${result.totalCount} memories${page} (showing ${result.entries.length}):\n${lines.join('\n')}${continuation}`
|
|
89
|
+
: `Found ${result.totalCount} memories${page}:\n${lines.join('\n')}${continuation}`
|
|
62
90
|
|
|
63
91
|
return {
|
|
64
92
|
success: true,
|
|
@@ -66,6 +94,9 @@ function defineSearchMemoryTool(search: SearchMemory): ToolDefinition {
|
|
|
66
94
|
data: {
|
|
67
95
|
entries: result.entries,
|
|
68
96
|
totalCount: result.totalCount,
|
|
97
|
+
truncated: result.truncated === true,
|
|
98
|
+
...(result.scannedCount !== undefined ? { scannedCount: result.scannedCount } : {}),
|
|
99
|
+
...(result.nextScanOffset !== undefined ? { nextScanOffset: result.nextScanOffset } : {}),
|
|
69
100
|
},
|
|
70
101
|
}
|
|
71
102
|
},
|