@namzu/sdk 38.1.0 → 38.2.1

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.
Files changed (90) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/manager/resident/agenda.d.ts +125 -0
  3. package/dist/manager/resident/agenda.d.ts.map +1 -0
  4. package/dist/manager/resident/agenda.js +551 -0
  5. package/dist/manager/resident/agenda.js.map +1 -0
  6. package/dist/manager/resident/delivery-window.d.ts +22 -0
  7. package/dist/manager/resident/delivery-window.d.ts.map +1 -0
  8. package/dist/manager/resident/delivery-window.js +78 -0
  9. package/dist/manager/resident/delivery-window.js.map +1 -0
  10. package/dist/manager/resident/host.d.ts +78 -0
  11. package/dist/manager/resident/host.d.ts.map +1 -0
  12. package/dist/manager/resident/host.js +252 -0
  13. package/dist/manager/resident/host.js.map +1 -0
  14. package/dist/manager/resident/initiative.d.ts +111 -0
  15. package/dist/manager/resident/initiative.d.ts.map +1 -0
  16. package/dist/manager/resident/initiative.js +121 -0
  17. package/dist/manager/resident/initiative.js.map +1 -0
  18. package/dist/manager/resident/learning.d.ts +483 -0
  19. package/dist/manager/resident/learning.d.ts.map +1 -0
  20. package/dist/manager/resident/learning.js +263 -0
  21. package/dist/manager/resident/learning.js.map +1 -0
  22. package/dist/manager/resident/loop.d.ts +31 -0
  23. package/dist/manager/resident/loop.d.ts.map +1 -0
  24. package/dist/manager/resident/loop.js +69 -0
  25. package/dist/manager/resident/loop.js.map +1 -0
  26. package/dist/manager/resident/outbox.d.ts +180 -0
  27. package/dist/manager/resident/outbox.d.ts.map +1 -0
  28. package/dist/manager/resident/outbox.js +214 -0
  29. package/dist/manager/resident/outbox.js.map +1 -0
  30. package/dist/manager/resident/proposal.d.ts +91 -0
  31. package/dist/manager/resident/proposal.d.ts.map +1 -0
  32. package/dist/manager/resident/proposal.js +75 -0
  33. package/dist/manager/resident/proposal.js.map +1 -0
  34. package/dist/manager/resident/store.d.ts +149 -0
  35. package/dist/manager/resident/store.d.ts.map +1 -0
  36. package/dist/manager/resident/store.js +175 -0
  37. package/dist/manager/resident/store.js.map +1 -0
  38. package/dist/prompt/contributions.d.ts +5 -2
  39. package/dist/prompt/contributions.d.ts.map +1 -1
  40. package/dist/prompt/contributions.js.map +1 -1
  41. package/dist/prompt/index.d.ts +2 -0
  42. package/dist/prompt/index.d.ts.map +1 -1
  43. package/dist/prompt/index.js +1 -0
  44. package/dist/prompt/index.js.map +1 -1
  45. package/dist/prompt/resident-step.d.ts +29 -0
  46. package/dist/prompt/resident-step.d.ts.map +1 -0
  47. package/dist/prompt/resident-step.js +74 -0
  48. package/dist/prompt/resident-step.js.map +1 -0
  49. package/dist/public-runtime.d.ts +10 -1
  50. package/dist/public-runtime.d.ts.map +1 -1
  51. package/dist/public-runtime.js +10 -1
  52. package/dist/public-runtime.js.map +1 -1
  53. package/dist/public-types.d.ts +16 -2
  54. package/dist/public-types.d.ts.map +1 -1
  55. package/dist/registry/index.d.ts +1 -1
  56. package/dist/registry/index.d.ts.map +1 -1
  57. package/dist/registry/tool/execute.d.ts +18 -0
  58. package/dist/registry/tool/execute.d.ts.map +1 -1
  59. package/dist/registry/tool/execute.js +43 -6
  60. package/dist/registry/tool/execute.js.map +1 -1
  61. package/dist/runtime/query/events.d.ts +6 -1
  62. package/dist/runtime/query/events.d.ts.map +1 -1
  63. package/dist/runtime/query/events.js +21 -12
  64. package/dist/runtime/query/events.js.map +1 -1
  65. package/dist/runtime/query/index.d.ts.map +1 -1
  66. package/dist/runtime/query/index.js +1 -1
  67. package/dist/runtime/query/index.js.map +1 -1
  68. package/dist/runtime/query/prompt-cache.d.ts.map +1 -1
  69. package/dist/runtime/query/prompt-cache.js +21 -42
  70. package/dist/runtime/query/prompt-cache.js.map +1 -1
  71. package/package.json +1 -1
  72. package/src/manager/resident/agenda.ts +853 -0
  73. package/src/manager/resident/delivery-window.ts +93 -0
  74. package/src/manager/resident/host.ts +356 -0
  75. package/src/manager/resident/initiative.ts +195 -0
  76. package/src/manager/resident/learning.ts +369 -0
  77. package/src/manager/resident/loop.ts +101 -0
  78. package/src/manager/resident/outbox.ts +307 -0
  79. package/src/manager/resident/proposal.ts +105 -0
  80. package/src/manager/resident/store.ts +229 -0
  81. package/src/prompt/contributions.ts +5 -2
  82. package/src/prompt/index.ts +2 -0
  83. package/src/prompt/resident-step.ts +96 -0
  84. package/src/public-runtime.ts +14 -0
  85. package/src/public-types.ts +67 -0
  86. package/src/registry/index.ts +1 -1
  87. package/src/registry/tool/execute.ts +56 -9
  88. package/src/runtime/query/events.ts +24 -14
  89. package/src/runtime/query/index.ts +1 -2
  90. package/src/runtime/query/prompt-cache.ts +24 -47
@@ -0,0 +1,307 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { z } from 'zod'
3
+ import type { ResidentAgendaState } from './agenda.js'
4
+ import { ResidentConflictError } from './store.js'
5
+
6
+ const time = z.number().int().nonnegative().safe().max(8_640_000_000_000_000)
7
+ const label = z.string().trim().min(1).max(256)
8
+
9
+ export const residentMessageInputSchema = z.object({
10
+ id: z.string().uuid(),
11
+ pursuitId: z.string().uuid(),
12
+ destination: label,
13
+ body: z
14
+ .string()
15
+ .min(1)
16
+ .max(8_000)
17
+ .refine((value) => value.trim().length > 0),
18
+ notBefore: time,
19
+ })
20
+
21
+ /** @experimental Host-approved content and an opaque destination route, never credentials. */
22
+ export type ResidentMessageInput = Readonly<z.infer<typeof residentMessageInputSchema>>
23
+
24
+ export const residentOutboxMessageSchema = residentMessageInputSchema
25
+ .extend({
26
+ tenantId: z.string().uuid(),
27
+ agentKey: z.string().min(1).max(200),
28
+ sourceClaimId: z.string().uuid().nullable(),
29
+ revision: z.number().int().positive().safe(),
30
+ attempts: z.number().int().nonnegative().safe(),
31
+ phase: z.enum(['pending', 'sending', 'acknowledged', 'cancelled']),
32
+ claimId: z.string().uuid().nullable(),
33
+ nextAttemptAt: time.nullable(),
34
+ receiptId: label.nullable(),
35
+ acknowledgedAt: time.nullable(),
36
+ lastReason: z.string().trim().min(1).max(1_000).nullable(),
37
+ })
38
+ .superRefine((message, context) => {
39
+ if (
40
+ (message.phase === 'sending') !== (message.claimId !== null) ||
41
+ (message.phase === 'pending') !== (message.nextAttemptAt !== null) ||
42
+ (message.phase === 'acknowledged') !== (message.receiptId !== null) ||
43
+ (message.phase === 'acknowledged') !== (message.acknowledgedAt !== null) ||
44
+ (message.nextAttemptAt !== null && message.nextAttemptAt < message.notBefore) ||
45
+ (message.phase !== 'pending' && message.attempts === 0)
46
+ )
47
+ context.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid outbox lifecycle.' })
48
+ })
49
+
50
+ /** @experimental Acknowledgment proves transport acceptance, not human reading. */
51
+ export type ResidentOutboxMessage = Readonly<z.infer<typeof residentOutboxMessageSchema>>
52
+
53
+ export const residentDeliveryOutcomeSchema = z.discriminatedUnion('kind', [
54
+ z.object({ kind: z.literal('acknowledged'), receiptId: label }),
55
+ z.object({
56
+ kind: z.literal('not-accepted'),
57
+ retryAt: time.nullable(),
58
+ reason: z.string().trim().min(1).max(1_000),
59
+ }),
60
+ ])
61
+
62
+ /** @experimental Retry only with evidence of non-acceptance; null retryAt ends delivery. */
63
+ export type ResidentDeliveryOutcome = Readonly<z.infer<typeof residentDeliveryOutcomeSchema>>
64
+
65
+ /** @experimental Atomic admission and exact-claim settlement, without automatic claim expiry. */
66
+ export interface ResidentOutboxStore {
67
+ read(): Promise<ResidentAgendaState | null>
68
+ claimMessage(
69
+ expected: ResidentAgendaState,
70
+ id: string,
71
+ now: number,
72
+ ): Promise<ResidentOutboxMessage>
73
+ settleMessage(
74
+ expected: ResidentOutboxMessage,
75
+ outcome: ResidentDeliveryOutcome,
76
+ now: number,
77
+ ): Promise<ResidentOutboxMessage>
78
+ }
79
+
80
+ /** @experimental A synchronous host policy, evaluated before claiming and again before sending. */
81
+ export type ResidentDeliveryGate = (
82
+ message: ResidentOutboxMessage,
83
+ now: number,
84
+ ) =>
85
+ | { readonly allow: true }
86
+ | {
87
+ readonly allow: false
88
+ readonly nextCheckAt: number | null
89
+ readonly reason: string
90
+ }
91
+
92
+ /** @experimental Bind an authorized route; forward message.id as the stable idempotency key. */
93
+ export type ResidentMessageTransport = (
94
+ message: ResidentOutboxMessage,
95
+ signal: AbortSignal,
96
+ ) => Promise<ResidentDeliveryOutcome>
97
+
98
+ /** @experimental One dispatch admission, with no model invocation, polling or implicit retries. */
99
+ export interface ResidentDeliveryOptions {
100
+ readonly signal: AbortSignal
101
+ readonly gate: ResidentDeliveryGate
102
+ readonly now?: () => number
103
+ }
104
+
105
+ /** @experimental An idle result never asserts a message reached its destination. */
106
+ export type ResidentDeliveryResult =
107
+ | { readonly status: 'settled'; readonly message: ResidentOutboxMessage }
108
+ | {
109
+ readonly status: 'idle'
110
+ readonly reason: 'paused' | 'unresolved' | 'empty' | 'not-due' | 'window' | 'contended'
111
+ readonly nextCheckAt: number | null
112
+ }
113
+
114
+ /** Internal pure enqueue used in the same revision transaction as pursuit settlement. */
115
+ export function appendResidentMessage(
116
+ state: ResidentAgendaState,
117
+ input: ResidentMessageInput,
118
+ sourceClaimId: string | null = null,
119
+ ): readonly ResidentOutboxMessage[] {
120
+ const checked = residentMessageInputSchema.parse(input)
121
+ if (!state.pursuits.some((p) => p.id === checked.pursuitId))
122
+ throw new Error('Outbox message references an unknown pursuit.')
123
+ const messages = state.outbox ?? []
124
+ const existing = messages.find((message) => message.id === checked.id)
125
+ if (existing) {
126
+ if (
127
+ existing.pursuitId !== checked.pursuitId ||
128
+ existing.destination !== checked.destination ||
129
+ existing.body !== checked.body ||
130
+ existing.notBefore !== checked.notBefore ||
131
+ existing.sourceClaimId !== sourceClaimId
132
+ )
133
+ throw new Error('Outbox message ID already names a different immutable intent.')
134
+ return messages
135
+ }
136
+ if (messages.length >= 128) throw new Error('Resident outbox capacity reached (128 intents).')
137
+ return [
138
+ ...messages,
139
+ Object.freeze(
140
+ residentOutboxMessageSchema.parse({
141
+ ...checked,
142
+ tenantId: state.tenantId,
143
+ agentKey: state.agentKey,
144
+ sourceClaimId,
145
+ revision: 1,
146
+ attempts: 0,
147
+ phase: 'pending',
148
+ claimId: null,
149
+ nextAttemptAt: checked.notBefore,
150
+ receiptId: null,
151
+ acknowledgedAt: null,
152
+ lastReason: null,
153
+ }),
154
+ ),
155
+ ]
156
+ }
157
+
158
+ /** Internal exact-message transition; agenda storage owns exclusive admission. */
159
+ export function claimResidentOutboxMessage(
160
+ message: ResidentOutboxMessage,
161
+ now: number,
162
+ ): ResidentOutboxMessage {
163
+ time.parse(now)
164
+ if (message.phase !== 'pending' || message.nextAttemptAt === null || message.nextAttemptAt > now)
165
+ throw new ResidentConflictError()
166
+ return Object.freeze(
167
+ residentOutboxMessageSchema.parse({
168
+ ...message,
169
+ revision: message.revision + 1,
170
+ attempts: message.attempts + 1,
171
+ phase: 'sending',
172
+ claimId: randomUUID(),
173
+ nextAttemptAt: null,
174
+ }),
175
+ )
176
+ }
177
+
178
+ /** Internal settlement; unknown failures deliberately have no transition. */
179
+ export function settleResidentOutboxMessage(
180
+ message: ResidentOutboxMessage,
181
+ outcome: ResidentDeliveryOutcome,
182
+ now: number,
183
+ ): ResidentOutboxMessage {
184
+ time.parse(now)
185
+ const checked = residentDeliveryOutcomeSchema.parse(outcome)
186
+ if (message.phase !== 'sending' || message.claimId === null) throw new ResidentConflictError()
187
+ if (checked.kind === 'not-accepted' && checked.retryAt !== null && checked.retryAt <= now)
188
+ throw new TypeError('A delivery retry must be scheduled in the future.')
189
+ return Object.freeze(
190
+ residentOutboxMessageSchema.parse({
191
+ ...message,
192
+ revision: message.revision + 1,
193
+ claimId: null,
194
+ ...(checked.kind === 'acknowledged'
195
+ ? { phase: 'acknowledged', receiptId: checked.receiptId, acknowledgedAt: now }
196
+ : {
197
+ phase: checked.retryAt === null ? 'cancelled' : 'pending',
198
+ nextAttemptAt: checked.retryAt,
199
+ lastReason: checked.reason,
200
+ }),
201
+ }),
202
+ )
203
+ }
204
+
205
+ function admission(gate: ResidentDeliveryGate, message: ResidentOutboxMessage, now: number) {
206
+ const result = gate(message, now)
207
+ if (result.allow === true) return result
208
+ if (
209
+ result.allow !== false ||
210
+ typeof result.reason !== 'string' ||
211
+ !result.reason.trim() ||
212
+ result.reason.trim().length > 1_000
213
+ )
214
+ throw new TypeError('Invalid resident delivery gate result.')
215
+ if (result.nextCheckAt !== null) {
216
+ time.parse(result.nextCheckAt)
217
+ if (result.nextCheckAt <= now) throw new TypeError('Delivery gate must defer into the future.')
218
+ }
219
+ return result
220
+ }
221
+
222
+ /**
223
+ * @experimental Persist one send claim before calling a host-owned transport.
224
+ * Throws, malformed outcomes and cancellation keep the claim unresolved. Stop
225
+ * old executors and inspect effects before explicit settleMessage reconciliation.
226
+ */
227
+ export async function deliverResidentMessage(
228
+ store: ResidentOutboxStore,
229
+ transport: ResidentMessageTransport,
230
+ options: ResidentDeliveryOptions,
231
+ ): Promise<ResidentDeliveryResult> {
232
+ const { signal, gate } = options
233
+ const clock = options.now ?? Date.now
234
+ const now = () => time.parse(clock())
235
+ signal.throwIfAborted()
236
+ const state = await store.read()
237
+ signal.throwIfAborted()
238
+ if (!state) throw new Error('Create the resident agenda before delivering messages.')
239
+ const idle = (
240
+ reason: Extract<ResidentDeliveryResult, { status: 'idle' }>['reason'],
241
+ nextCheckAt: number | null = null,
242
+ ): ResidentDeliveryResult => ({ status: 'idle', reason, nextCheckAt })
243
+ if (state.paused) return idle('paused')
244
+ const messages = state.outbox ?? []
245
+ if (messages.some((message) => message.phase === 'sending')) return idle('unresolved')
246
+ const pending = messages.filter((message) => message.phase === 'pending')
247
+ if (!pending.length) return idle('empty')
248
+ let nextCheckAt: number | null = null
249
+ let gated = false
250
+ const at = now()
251
+ for (const message of pending.sort(
252
+ (a, b) => (a.nextAttemptAt ?? 0) - (b.nextAttemptAt ?? 0) || a.id.localeCompare(b.id),
253
+ )) {
254
+ if (message.nextAttemptAt !== null && message.nextAttemptAt > at) {
255
+ nextCheckAt = Math.min(nextCheckAt ?? Number.POSITIVE_INFINITY, message.nextAttemptAt)
256
+ continue
257
+ }
258
+ const permission = admission(gate, message, at)
259
+ signal.throwIfAborted()
260
+ if (!permission.allow) {
261
+ gated = true
262
+ if (permission.nextCheckAt !== null)
263
+ nextCheckAt = Math.min(nextCheckAt ?? Number.POSITIVE_INFINITY, permission.nextCheckAt)
264
+ continue
265
+ }
266
+ let claimed: ResidentOutboxMessage
267
+ try {
268
+ claimed = await store.claimMessage(state, message.id, now())
269
+ } catch (error) {
270
+ if (error instanceof ResidentConflictError) return idle('contended')
271
+ throw error
272
+ }
273
+ signal.throwIfAborted()
274
+ const fresh = await store.read()
275
+ signal.throwIfAborted()
276
+ const current = fresh?.outbox?.find((item) => item.id === claimed.id)
277
+ if (!current || current.revision !== claimed.revision || current.claimId !== claimed.claimId)
278
+ return idle('contended')
279
+ // A pause or closing window during admission must not start the transport.
280
+ const checkedAt = now()
281
+ const permissionNow = fresh?.paused
282
+ ? {
283
+ allow: false as const,
284
+ nextCheckAt: checkedAt + 1,
285
+ reason: 'Resident agenda paused before send.',
286
+ }
287
+ : admission(gate, claimed, checkedAt)
288
+ if (!permissionNow.allow) {
289
+ // Transport has not been entered: scheduling another check has no remote effect.
290
+ await store.settleMessage(
291
+ claimed,
292
+ {
293
+ kind: 'not-accepted',
294
+ retryAt: permissionNow.nextCheckAt ?? checkedAt + 1,
295
+ reason: permissionNow.reason,
296
+ },
297
+ checkedAt,
298
+ )
299
+ return idle(fresh?.paused ? 'paused' : 'window', permissionNow.nextCheckAt)
300
+ }
301
+ signal.throwIfAborted()
302
+ const outcome = await transport(claimed, signal)
303
+ signal.throwIfAborted()
304
+ return { status: 'settled', message: await store.settleMessage(claimed, outcome, now()) }
305
+ }
306
+ return idle(gated ? 'window' : 'not-due', nextCheckAt)
307
+ }
@@ -0,0 +1,105 @@
1
+ import { z } from 'zod'
2
+ import type { ResidentAgendaState } from './agenda.js'
3
+
4
+ const identifier = z.string().uuid()
5
+ const revision = z.number().int().positive().safe()
6
+ const domain = z
7
+ .string()
8
+ .min(1)
9
+ .max(120)
10
+ .refine((value) => value.trim().length > 0, 'A proposal domain must contain text.')
11
+ const reason = z.string().trim().min(1).max(1_000)
12
+ const evidenceKey = z.string().trim().min(1).max(256)
13
+
14
+ export const residentProposalSchema = z.object({
15
+ id: identifier,
16
+ parentId: identifier,
17
+ parentRevision: revision,
18
+ domain,
19
+ objective: z.string().trim().min(1).max(8_000),
20
+ reason,
21
+ evidenceKey,
22
+ })
23
+
24
+ export const residentProposalLimitsSchema = z.object({
25
+ domains: z.array(domain).min(1),
26
+ maxChildrenPerParent: z.number().int().min(1).max(8),
27
+ maxDepth: z.number().int().min(1).max(4),
28
+ })
29
+
30
+ /** @experimental An inert subgoal proposal; creating it grants no execution authority. */
31
+ export interface ResidentProposal {
32
+ readonly id: string
33
+ readonly parentId: string
34
+ readonly parentRevision: number
35
+ readonly domain: string
36
+ readonly objective: string
37
+ readonly reason: string
38
+ readonly evidenceKey: string
39
+ }
40
+
41
+ /** @experimental Host-approved routing domains and finite subgoal admission bounds. */
42
+ export interface ResidentProposalLimits {
43
+ readonly domains: readonly string[]
44
+ readonly maxChildrenPerParent: number
45
+ readonly maxDepth: number
46
+ }
47
+
48
+ /** Persisted provenance shared with the agenda's schema validation. */
49
+ export const residentProposalOriginSchema = z.object({
50
+ proposalId: identifier,
51
+ parentId: identifier,
52
+ parentRevision: revision,
53
+ domain,
54
+ reason,
55
+ evidenceKey,
56
+ depth: z.number().int().min(1).max(4),
57
+ })
58
+
59
+ /** @experimental Host-admitted ancestry and evidence for a resident subgoal. */
60
+ export type ResidentProposalOrigin = Readonly<z.infer<typeof residentProposalOriginSchema>>
61
+
62
+ /**
63
+ * @experimental Pure validation against one agenda snapshot. The host must
64
+ * atomically bind this snapshot to admission. Domains are routing metadata;
65
+ * this does not prove an objective fits its domain or authorize any tools.
66
+ */
67
+ export function validateResidentProposal(
68
+ agenda: ResidentAgendaState,
69
+ input: ResidentProposal,
70
+ limits: ResidentProposalLimits,
71
+ ): ResidentProposalOrigin {
72
+ const proposal = residentProposalSchema.parse(input)
73
+ const policy = residentProposalLimitsSchema.parse(limits)
74
+ if (agenda.paused) throw new Error('A paused agenda cannot admit resident proposals.')
75
+ if (agenda.pursuits.length >= 32) throw new Error('Resident agenda pursuit bound reached.')
76
+ if (agenda.pursuits.some((pursuit) => pursuit.origin?.proposalId === proposal.id))
77
+ throw new Error('Resident proposal has already been admitted.')
78
+ const parent = agenda.pursuits.find((pursuit) => pursuit.id === proposal.parentId)
79
+ if (!parent) throw new Error('Unknown resident proposal parent.')
80
+ if (parent.state.revision !== proposal.parentRevision)
81
+ throw new Error('Resident proposal parent revision is stale.')
82
+ if (parent.state.phase !== 'waiting' && parent.state.phase !== 'complete')
83
+ throw new Error('Resident proposal parent must be waiting or complete.')
84
+ if (!policy.domains.includes(proposal.domain))
85
+ throw new Error('Resident proposal domain is not host-approved.')
86
+ if (
87
+ agenda.pursuits.filter((pursuit) => pursuit.origin?.parentId === parent.id).length +
88
+ (parent.retiredChildren ?? 0) >=
89
+ policy.maxChildrenPerParent
90
+ )
91
+ throw new Error('Resident proposal parent child bound reached.')
92
+ const depth = (parent.origin?.depth ?? 0) + 1
93
+ if (depth > policy.maxDepth) throw new Error('Resident proposal depth bound reached.')
94
+ return Object.freeze(
95
+ residentProposalOriginSchema.parse({
96
+ proposalId: proposal.id,
97
+ parentId: proposal.parentId,
98
+ parentRevision: proposal.parentRevision,
99
+ domain: proposal.domain,
100
+ reason: proposal.reason,
101
+ evidenceKey: proposal.evidenceKey,
102
+ depth,
103
+ }),
104
+ )
105
+ }
@@ -0,0 +1,229 @@
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import { join } from 'node:path'
3
+ import { z } from 'zod'
4
+ import {
5
+ DiskRevisionRecordStore,
6
+ revisionFileSegment,
7
+ } from '../../store/kv/revision-record-store.js'
8
+ import { defineSchema } from '../../store/schema.js'
9
+ import type { TenantId } from '../../types/ids/index.js'
10
+ import { asTenantId } from '../../utils/id.js'
11
+
12
+ const text = z.string().trim().min(1).max(8_000)
13
+ const time = z.number().int().nonnegative().safe()
14
+ export const residentDecisionSchema = z.discriminatedUnion('kind', [
15
+ z.object({ kind: z.literal('wait'), summary: text, wakeAt: time.nullable() }),
16
+ z.object({ kind: z.literal('complete'), summary: text }),
17
+ z.object({ kind: z.literal('blocked'), summary: text }),
18
+ ])
19
+
20
+ /** @experimental One bounded step's durable disposition; silence is not completion. */
21
+ export type ResidentDecision = z.infer<typeof residentDecisionSchema>
22
+
23
+ export const residentStateSchema = z
24
+ .object({
25
+ tenantId: z.string().uuid(),
26
+ agentKey: z.string().min(1).max(200),
27
+ pursuitId: z.string().uuid().optional(),
28
+ identity: text,
29
+ objective: text,
30
+ revision: z.number().int().positive().safe(),
31
+ stepsAdmitted: time,
32
+ phase: z.enum(['waiting', 'running', 'complete', 'blocked']),
33
+ wakeAt: time.nullable(),
34
+ reason: text,
35
+ summary: text.nullable(),
36
+ claimId: z.string().uuid().nullable(),
37
+ })
38
+ .superRefine((state, context) => {
39
+ if ((state.phase === 'running') !== (state.claimId !== null)) {
40
+ context.addIssue({
41
+ code: z.ZodIssueCode.custom,
42
+ message: 'Running state requires a claim.',
43
+ })
44
+ }
45
+ if (state.phase !== 'waiting' && state.wakeAt !== null) {
46
+ context.addIssue({
47
+ code: z.ZodIssueCode.custom,
48
+ message: 'Only waiting state may have a wake time.',
49
+ })
50
+ }
51
+ })
52
+
53
+ /** @experimental Persisted identity and one pursuit, independent of a conversation. */
54
+ export type ResidentState = Readonly<z.infer<typeof residentStateSchema>>
55
+
56
+ /** Bounded addressing for local resident state. Stored scope is still validated. */
57
+ export function residentKeySegment(key: string): string {
58
+ const encoded = revisionFileSegment(key)
59
+ return encoded.length <= 255
60
+ ? encoded
61
+ : `~sha256-${createHash('sha256').update(encoded).digest('hex')}`
62
+ }
63
+
64
+ /** @experimental Minimal atomic contract needed to execute an existing pursuit. */
65
+ export type ResidentExecutionStore = Pick<ResidentStore, 'read' | 'claim' | 'settle'>
66
+
67
+ /** @experimental Backends must atomically compare revisions and publish admission before work. */
68
+ export interface ResidentStore {
69
+ read(): Promise<ResidentState | null>
70
+ create(identity: string, objective: string): Promise<ResidentState>
71
+ claim(expected: ResidentState, now: number): Promise<ResidentState>
72
+ settle(expected: ResidentState, decision: ResidentDecision, now: number): Promise<ResidentState>
73
+ wake(expected: ResidentState, reason: string, now: number): Promise<ResidentState>
74
+ }
75
+
76
+ /** A competing owner advanced this resident's immutable revision. */
77
+ export class ResidentConflictError extends Error {
78
+ constructor() {
79
+ super('Resident state changed; read the current state before retrying.')
80
+ this.name = 'ResidentConflictError'
81
+ }
82
+ }
83
+
84
+ /**
85
+ * @experimental Local-filesystem state for one tenant/agent key and one pursuit.
86
+ * A running claim never expires automatically: a crashed step may have effects.
87
+ * Uses exclusive immutable revision publication, not a read/rename lock.
88
+ */
89
+ export class DiskResidentStore implements ResidentStore {
90
+ private readonly records = new DiskRevisionRecordStore<ResidentState>(
91
+ defineSchema({ kind: 'resident', current: 1, migrations: {} }),
92
+ 'resident store',
93
+ (record) => record.revision,
94
+ )
95
+ private readonly location
96
+ private readonly tenantId: TenantId
97
+ private readonly agentKey: string
98
+
99
+ constructor(root: string, scope: { tenantId: TenantId; agentKey: string }) {
100
+ this.tenantId = asTenantId(scope.tenantId)
101
+ this.agentKey = z.string().min(1).max(200).parse(scope.agentKey)
102
+ const directory = join(root, this.tenantId, residentKeySegment(this.agentKey))
103
+ this.location = {
104
+ legacyPath: join(directory, 'state.json'),
105
+ revisionsDir: join(directory, 'revisions'),
106
+ publishLegacyProjection: false,
107
+ }
108
+ }
109
+
110
+ private checked(record: ResidentState): ResidentState {
111
+ const state = residentStateSchema.parse(record)
112
+ if (
113
+ state.pursuitId !== undefined ||
114
+ state.tenantId !== this.tenantId ||
115
+ state.agentKey !== this.agentKey
116
+ ) {
117
+ throw new Error('Resident record does not match the bound tenant and agent.')
118
+ }
119
+ return Object.freeze(state)
120
+ }
121
+
122
+ async read(): Promise<ResidentState | null> {
123
+ const state = await this.records.read(this.location)
124
+ return state === null ? null : this.checked(state)
125
+ }
126
+
127
+ async create(identity: string, objective: string): Promise<ResidentState> {
128
+ const initial = this.checked({
129
+ tenantId: this.tenantId,
130
+ agentKey: this.agentKey,
131
+ identity,
132
+ objective,
133
+ revision: 1,
134
+ stepsAdmitted: 0,
135
+ phase: 'waiting',
136
+ wakeAt: 0,
137
+ reason: 'Initial pursuit',
138
+ summary: null,
139
+ claimId: null,
140
+ })
141
+ return this.records.transact(this.location, (current) => {
142
+ if (current !== null) throw new ResidentConflictError()
143
+ return { record: initial, result: initial }
144
+ })
145
+ }
146
+
147
+ private async change(
148
+ expected: ResidentState,
149
+ mutate: (current: ResidentState) => ResidentState,
150
+ ): Promise<ResidentState> {
151
+ this.checked(expected)
152
+ return this.records.transact(this.location, (record) => {
153
+ if (record === null) throw new ResidentConflictError()
154
+ const current = this.checked(record)
155
+ if (current.revision !== expected.revision || current.claimId !== expected.claimId) {
156
+ throw new ResidentConflictError()
157
+ }
158
+ const next = this.checked({
159
+ ...mutate(current),
160
+ revision: current.revision + 1,
161
+ })
162
+ return { record: next, result: next }
163
+ })
164
+ }
165
+
166
+ /** Persist admission before invoking a model, tool or developer callback. */
167
+ async claim(expected: ResidentState, now: number): Promise<ResidentState> {
168
+ return this.change(expected, (state) => claimResidentState(state, now))
169
+ }
170
+
171
+ /** Only the exact admitted revision may settle; late owners cannot overwrite recovery. */
172
+ async settle(
173
+ expected: ResidentState,
174
+ decision: ResidentDecision,
175
+ now: number,
176
+ ): Promise<ResidentState> {
177
+ return this.change(expected, (state) => settleResidentState(state, decision, now))
178
+ }
179
+
180
+ /** Host-supplied new evidence can wake a waiting pursuit; terminal work stays terminal. */
181
+ async wake(expected: ResidentState, reason: string, now: number): Promise<ResidentState> {
182
+ return this.change(expected, (state) => wakeResidentState(state, reason, now))
183
+ }
184
+ }
185
+
186
+ /** Internal transitions shared by single-pursuit and whole-agent admission. */
187
+ export function claimResidentState(state: ResidentState, now: number): ResidentState {
188
+ time.parse(now)
189
+ if (state.phase !== 'waiting' || state.wakeAt === null || state.wakeAt > now)
190
+ throw new Error('Resident is not due.')
191
+ return {
192
+ ...state,
193
+ phase: 'running',
194
+ wakeAt: null,
195
+ claimId: randomUUID(),
196
+ stepsAdmitted: state.stepsAdmitted + 1,
197
+ }
198
+ }
199
+
200
+ export function settleResidentState(
201
+ state: ResidentState,
202
+ decision: ResidentDecision,
203
+ now: number,
204
+ ): ResidentState {
205
+ time.parse(now)
206
+ const outcome = residentDecisionSchema.parse(decision)
207
+ if (outcome.kind === 'wait' && outcome.wakeAt !== null && outcome.wakeAt <= now)
208
+ throw new Error('A scheduled continuation must be in the future.')
209
+ if (state.phase !== 'running') throw new Error('Resident has no admitted step to settle.')
210
+ return {
211
+ ...state,
212
+ phase: outcome.kind === 'wait' ? 'waiting' : outcome.kind,
213
+ wakeAt: outcome.kind === 'wait' ? outcome.wakeAt : null,
214
+ summary: outcome.summary,
215
+ reason: 'Scheduled continuation',
216
+ claimId: null,
217
+ }
218
+ }
219
+
220
+ export function wakeResidentState(
221
+ state: ResidentState,
222
+ reason: string,
223
+ now: number,
224
+ ): ResidentState {
225
+ time.parse(now)
226
+ const evidence = text.parse(reason)
227
+ if (state.phase !== 'waiting') throw new Error('Only a waiting resident can be woken.')
228
+ return { ...state, wakeAt: now, reason: evidence }
229
+ }
@@ -41,7 +41,10 @@ export interface PromptContributionContext {
41
41
  *
42
42
  * Not cosmetic, and the wrong answer is expensive in a way nothing reports.
43
43
  * `static` is the segment the prompt cache keeps and a provider caches
44
- * across turns; `dynamic` is re-sent every iteration. A contributor whose
44
+ * across turns; `dynamic` is the uncached invocation snapshot. `query`
45
+ * assembles both once at invocation start and sends them on every iteration.
46
+ * Use `turn` for a renderer that must observe changes during the invocation.
47
+ * A contributor whose
45
48
  * text varies per turn but declares `static` either invalidates the cached
46
49
  * prefix on every iteration — paying full price for a cache that never
47
50
  * hits — or, worse, gets served the first turn's text forever.
@@ -55,7 +58,7 @@ export type PromptPlacement = 'static' | 'dynamic' | 'turn'
55
58
  * `turn` is a third thing, not a looser `dynamic`.
56
59
  *
57
60
  * `static` and `dynamic` are both parts of the SYSTEM PROMPT: assembled
58
- * once per request, sent as system messages, and — for `static` — cached by
61
+ * once per invocation, sent as system messages, and — for `static` — cached by
59
62
  * the provider across turns. `turn` is not in the system prompt at all. It
60
63
  * rides the ephemeral trailing message that a step's guidance, its skills
61
64
  * and the approval-policy notice already use: appended to the request,
@@ -21,3 +21,5 @@ export {
21
21
  codingAgentDoctrineContribution,
22
22
  } from './coding-agent-doctrine.js'
23
23
  export type { CodingAgentDoctrineOptions } from './coding-agent-doctrine.js'
24
+ export { createResidentStepContributions } from './resident-step.js'
25
+ export type { ResidentStepPromptOptions } from './resident-step.js'