@namzu/sdk 38.0.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 (157) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  3. package/dist/agents/ReactiveAgent.js +1 -0
  4. package/dist/agents/ReactiveAgent.js.map +1 -1
  5. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  6. package/dist/bridge/sse/mapper.js +2 -0
  7. package/dist/bridge/sse/mapper.js.map +1 -1
  8. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  9. package/dist/manager/agent/lifecycle.js +4 -0
  10. package/dist/manager/agent/lifecycle.js.map +1 -1
  11. package/dist/manager/plan/lifecycle.d.ts +1 -1
  12. package/dist/manager/plan/lifecycle.d.ts.map +1 -1
  13. package/dist/manager/plan/lifecycle.js +14 -12
  14. package/dist/manager/plan/lifecycle.js.map +1 -1
  15. package/dist/manager/resident/agenda.d.ts +125 -0
  16. package/dist/manager/resident/agenda.d.ts.map +1 -0
  17. package/dist/manager/resident/agenda.js +551 -0
  18. package/dist/manager/resident/agenda.js.map +1 -0
  19. package/dist/manager/resident/delivery-window.d.ts +22 -0
  20. package/dist/manager/resident/delivery-window.d.ts.map +1 -0
  21. package/dist/manager/resident/delivery-window.js +78 -0
  22. package/dist/manager/resident/delivery-window.js.map +1 -0
  23. package/dist/manager/resident/host.d.ts +78 -0
  24. package/dist/manager/resident/host.d.ts.map +1 -0
  25. package/dist/manager/resident/host.js +252 -0
  26. package/dist/manager/resident/host.js.map +1 -0
  27. package/dist/manager/resident/initiative.d.ts +111 -0
  28. package/dist/manager/resident/initiative.d.ts.map +1 -0
  29. package/dist/manager/resident/initiative.js +121 -0
  30. package/dist/manager/resident/initiative.js.map +1 -0
  31. package/dist/manager/resident/learning.d.ts +483 -0
  32. package/dist/manager/resident/learning.d.ts.map +1 -0
  33. package/dist/manager/resident/learning.js +263 -0
  34. package/dist/manager/resident/learning.js.map +1 -0
  35. package/dist/manager/resident/loop.d.ts +31 -0
  36. package/dist/manager/resident/loop.d.ts.map +1 -0
  37. package/dist/manager/resident/loop.js +69 -0
  38. package/dist/manager/resident/loop.js.map +1 -0
  39. package/dist/manager/resident/outbox.d.ts +180 -0
  40. package/dist/manager/resident/outbox.d.ts.map +1 -0
  41. package/dist/manager/resident/outbox.js +214 -0
  42. package/dist/manager/resident/outbox.js.map +1 -0
  43. package/dist/manager/resident/proposal.d.ts +91 -0
  44. package/dist/manager/resident/proposal.d.ts.map +1 -0
  45. package/dist/manager/resident/proposal.js +75 -0
  46. package/dist/manager/resident/proposal.js.map +1 -0
  47. package/dist/manager/resident/store.d.ts +149 -0
  48. package/dist/manager/resident/store.d.ts.map +1 -0
  49. package/dist/manager/resident/store.js +175 -0
  50. package/dist/manager/resident/store.js.map +1 -0
  51. package/dist/prompt/contributions.d.ts +5 -2
  52. package/dist/prompt/contributions.d.ts.map +1 -1
  53. package/dist/prompt/contributions.js.map +1 -1
  54. package/dist/prompt/index.d.ts +2 -0
  55. package/dist/prompt/index.d.ts.map +1 -1
  56. package/dist/prompt/index.js +1 -0
  57. package/dist/prompt/index.js.map +1 -1
  58. package/dist/prompt/resident-step.d.ts +29 -0
  59. package/dist/prompt/resident-step.d.ts.map +1 -0
  60. package/dist/prompt/resident-step.js +74 -0
  61. package/dist/prompt/resident-step.js.map +1 -0
  62. package/dist/provider/capabilities.d.ts +1 -1
  63. package/dist/provider/capabilities.d.ts.map +1 -1
  64. package/dist/provider/capabilities.js +4 -1
  65. package/dist/provider/capabilities.js.map +1 -1
  66. package/dist/provider/fallback.d.ts.map +1 -1
  67. package/dist/provider/fallback.js +2 -0
  68. package/dist/provider/fallback.js.map +1 -1
  69. package/dist/provider/idle-timeout.d.ts.map +1 -1
  70. package/dist/provider/idle-timeout.js +5 -0
  71. package/dist/provider/idle-timeout.js.map +1 -1
  72. package/dist/provider/retry.d.ts.map +1 -1
  73. package/dist/provider/retry.js +5 -0
  74. package/dist/provider/retry.js.map +1 -1
  75. package/dist/public-runtime.d.ts +10 -1
  76. package/dist/public-runtime.d.ts.map +1 -1
  77. package/dist/public-runtime.js +10 -1
  78. package/dist/public-runtime.js.map +1 -1
  79. package/dist/public-types.d.ts +16 -2
  80. package/dist/public-types.d.ts.map +1 -1
  81. package/dist/registry/index.d.ts +1 -1
  82. package/dist/registry/index.d.ts.map +1 -1
  83. package/dist/registry/tool/execute.d.ts +18 -0
  84. package/dist/registry/tool/execute.d.ts.map +1 -1
  85. package/dist/registry/tool/execute.js +43 -6
  86. package/dist/registry/tool/execute.js.map +1 -1
  87. package/dist/runtime/query/events.d.ts +6 -1
  88. package/dist/runtime/query/events.d.ts.map +1 -1
  89. package/dist/runtime/query/events.js +21 -12
  90. package/dist/runtime/query/events.js.map +1 -1
  91. package/dist/runtime/query/index.d.ts.map +1 -1
  92. package/dist/runtime/query/index.js +1 -1
  93. package/dist/runtime/query/index.js.map +1 -1
  94. package/dist/runtime/query/prompt-cache.d.ts.map +1 -1
  95. package/dist/runtime/query/prompt-cache.js +21 -42
  96. package/dist/runtime/query/prompt-cache.js.map +1 -1
  97. package/dist/scheduler/local.d.ts.map +1 -1
  98. package/dist/scheduler/local.js +2 -0
  99. package/dist/scheduler/local.js.map +1 -1
  100. package/dist/testing.d.ts +1 -0
  101. package/dist/testing.d.ts.map +1 -1
  102. package/dist/testing.js +1 -0
  103. package/dist/testing.js.map +1 -1
  104. package/dist/tools/coordinator/index.d.ts.map +1 -1
  105. package/dist/tools/coordinator/index.js +3 -0
  106. package/dist/tools/coordinator/index.js.map +1 -1
  107. package/dist/types/agent/reactive.d.ts +4 -0
  108. package/dist/types/agent/reactive.d.ts.map +1 -1
  109. package/dist/types/agent/scheduler.d.ts +3 -0
  110. package/dist/types/agent/scheduler.d.ts.map +1 -1
  111. package/dist/types/agent/task.d.ts +3 -0
  112. package/dist/types/agent/task.d.ts.map +1 -1
  113. package/dist/types/provider/interface.d.ts +2 -0
  114. package/dist/types/provider/interface.d.ts.map +1 -1
  115. package/dist/types/run/events.d.ts +3 -0
  116. package/dist/types/run/events.d.ts.map +1 -1
  117. package/dist/types/run/events.js.map +1 -1
  118. package/dist/types/sandbox/index.d.ts +36 -3
  119. package/dist/types/sandbox/index.d.ts.map +1 -1
  120. package/dist/types/sandbox/index.js.map +1 -1
  121. package/package.json +1 -1
  122. package/src/agents/ReactiveAgent.ts +1 -0
  123. package/src/bridge/sse/mapper.ts +2 -0
  124. package/src/manager/agent/lifecycle.ts +4 -0
  125. package/src/manager/plan/lifecycle.ts +15 -13
  126. package/src/manager/resident/agenda.ts +853 -0
  127. package/src/manager/resident/delivery-window.ts +93 -0
  128. package/src/manager/resident/host.ts +356 -0
  129. package/src/manager/resident/initiative.ts +195 -0
  130. package/src/manager/resident/learning.ts +369 -0
  131. package/src/manager/resident/loop.ts +101 -0
  132. package/src/manager/resident/outbox.ts +307 -0
  133. package/src/manager/resident/proposal.ts +105 -0
  134. package/src/manager/resident/store.ts +229 -0
  135. package/src/prompt/contributions.ts +5 -2
  136. package/src/prompt/index.ts +2 -0
  137. package/src/prompt/resident-step.ts +96 -0
  138. package/src/provider/capabilities.ts +8 -3
  139. package/src/provider/fallback.ts +6 -0
  140. package/src/provider/idle-timeout.ts +6 -0
  141. package/src/provider/retry.ts +6 -0
  142. package/src/public-runtime.ts +14 -0
  143. package/src/public-types.ts +67 -0
  144. package/src/registry/index.ts +1 -1
  145. package/src/registry/tool/execute.ts +56 -9
  146. package/src/runtime/query/events.ts +24 -14
  147. package/src/runtime/query/index.ts +1 -2
  148. package/src/runtime/query/prompt-cache.ts +24 -47
  149. package/src/scheduler/local.ts +2 -0
  150. package/src/testing.ts +2 -0
  151. package/src/tools/coordinator/index.ts +3 -0
  152. package/src/types/agent/reactive.ts +2 -0
  153. package/src/types/agent/scheduler.ts +4 -0
  154. package/src/types/agent/task.ts +4 -0
  155. package/src/types/provider/interface.ts +3 -0
  156. package/src/types/run/events.ts +3 -0
  157. package/src/types/sandbox/index.ts +42 -4
@@ -0,0 +1,93 @@
1
+ import type { ResidentDeliveryGate } from './outbox.js'
2
+
3
+ /** @experimental A daily allowed delivery window in an explicit named time zone. */
4
+ export interface ResidentDeliveryWindowConfig {
5
+ readonly timeZone: string
6
+ /** Inclusive local minute of the day, from 0 through 1439. */
7
+ readonly startMinute: number
8
+ /** Exclusive local minute of the day; less than startMinute means overnight. */
9
+ readonly endMinute: number
10
+ }
11
+
12
+ const minuteMs = 60_000
13
+ const searchMinutes = 72 * 60
14
+
15
+ /**
16
+ * @experimental Admit delivery within a daily local-time window, without model
17
+ * calls. The start is inclusive and the end exclusive; equal boundaries are
18
+ * rejected. Omit this gate when no time restriction is wanted.
19
+ *
20
+ * Future admission searches actual UTC minute boundaries, so nonexistent local
21
+ * times are skipped and repeated times can open the window twice. The search
22
+ * is bounded to 72 hours and the Date range; no opening returns nextCheckAt:null.
23
+ * Historic time zones with second-based offsets may defer up to one extra minute.
24
+ * This gate does not schedule a timer, authorize a destination or send a message.
25
+ */
26
+ export function createResidentDeliveryWindow(
27
+ config: ResidentDeliveryWindowConfig,
28
+ ): ResidentDeliveryGate {
29
+ if (
30
+ !config ||
31
+ typeof config.timeZone !== 'string' ||
32
+ config.timeZone.length === 0 ||
33
+ config.timeZone !== config.timeZone.trim() ||
34
+ /^[+-]/.test(config.timeZone) ||
35
+ !Number.isInteger(config.startMinute) ||
36
+ config.startMinute < 0 ||
37
+ config.startMinute > 1439 ||
38
+ !Number.isInteger(config.endMinute) ||
39
+ config.endMinute < 0 ||
40
+ config.endMinute > 1439 ||
41
+ config.startMinute === config.endMinute
42
+ ) {
43
+ throw new TypeError('Delivery window requires a named time zone and distinct minutes 0–1439.')
44
+ }
45
+
46
+ let formatter: Intl.DateTimeFormat
47
+ try {
48
+ formatter = new Intl.DateTimeFormat('en-US', {
49
+ timeZone: config.timeZone,
50
+ hour: '2-digit',
51
+ minute: '2-digit',
52
+ hourCycle: 'h23',
53
+ })
54
+ } catch {
55
+ throw new TypeError('Delivery window requires a valid named time zone.')
56
+ }
57
+ const { startMinute, endMinute } = config
58
+ const timeZone = formatter.resolvedOptions().timeZone
59
+ const allowed = (at: number): boolean => {
60
+ const parts = formatter.formatToParts(at)
61
+ const hour = Number(parts.find((part) => part.type === 'hour')?.value)
62
+ const minute = Number(parts.find((part) => part.type === 'minute')?.value)
63
+ const localMinute = hour * 60 + minute
64
+ return startMinute < endMinute
65
+ ? localMinute >= startMinute && localMinute < endMinute
66
+ : localMinute >= startMinute || localMinute < endMinute
67
+ }
68
+
69
+ return (_message, now) => {
70
+ if (!Number.isFinite(now) || !Number.isFinite(new Date(now).getTime())) {
71
+ throw new TypeError('Delivery window requires a finite timestamp within the Date range.')
72
+ }
73
+ if (allowed(now)) return { allow: true }
74
+
75
+ const firstMinute = (Math.floor(now / minuteMs) + 1) * minuteMs
76
+ for (let offset = 0; offset < searchMinutes; offset++) {
77
+ const candidate = firstMinute + offset * minuteMs
78
+ if (!Number.isFinite(new Date(candidate).getTime())) break
79
+ if (allowed(candidate)) {
80
+ return {
81
+ allow: false,
82
+ nextCheckAt: candidate,
83
+ reason: `Outside the allowed daily delivery window in ${timeZone}.`,
84
+ }
85
+ }
86
+ }
87
+ return {
88
+ allow: false,
89
+ nextCheckAt: null,
90
+ reason: `No allowed delivery minute found within the next 72 hours and Date range in ${timeZone}.`,
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,356 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises'
2
+ import type { ResidentAgendaState, ResidentAgendaStore, ResidentPursuit } from './agenda.js'
3
+ import {
4
+ type ResidentObservation,
5
+ type ResidentSelection,
6
+ type ResidentSelector,
7
+ residentObservationSchema,
8
+ } from './initiative.js'
9
+ import { type ResidentLearningState, freezeResidentLearning } from './learning.js'
10
+ import { type ResidentStep, stepResident } from './loop.js'
11
+ import { type ResidentMessageInput, residentMessageInputSchema } from './outbox.js'
12
+ import { ResidentConflictError, type ResidentDecision, residentDecisionSchema } from './store.js'
13
+
14
+ /** @experimental Host result describes execution, not external-effect rollback. */
15
+ export interface ResidentHostResult {
16
+ readonly status: 'idle' | 'paused' | 'unresolved' | 'cancelled' | 'limit' | 'contended'
17
+ readonly stepsSettled: number
18
+ readonly nextWakeAt: number | null
19
+ readonly selection?: ResidentSelection
20
+ }
21
+
22
+ /** @experimental Explicit authorization to drive a finite number of background steps. */
23
+ export interface ResidentHostRunOptions {
24
+ readonly signal: AbortSignal
25
+ readonly maxSteps: number
26
+ /** Maximum single idle wait; with keepAlive, also bounds local agenda polling. */
27
+ readonly maxIdleMs?: number
28
+ /** Keep waiting for useful work within this invocation's finite step cap. Default false. */
29
+ readonly keepAlive?: boolean
30
+ }
31
+
32
+ /** @experimental Immutable context from the agenda snapshot that authorized this admission. */
33
+ export interface ResidentStepContext {
34
+ readonly agendaRevision: number
35
+ readonly learning?: ResidentLearningState
36
+ }
37
+
38
+ /** @experimental A developer callback may bind a different SDK run for each pursuit. */
39
+ export type ResidentPursuitStep = (
40
+ pursuit: ResidentPursuit,
41
+ signal: AbortSignal,
42
+ ) => ReturnType<ResidentStep>
43
+
44
+ /** @experimental Context-aware callback; existing two-argument steps remain assignable. */
45
+ export type ResidentContextualStep = (
46
+ pursuit: ResidentPursuit,
47
+ signal: AbortSignal,
48
+ context: ResidentStepContext,
49
+ ) => ReturnType<ResidentStep>
50
+
51
+ /** @experimental Inspect actual outcomes and resource receipts independently of model prose. */
52
+ export type ResidentObserver = (
53
+ pursuit: ResidentPursuit,
54
+ decision: ResidentDecision,
55
+ signal: AbortSignal,
56
+ ) => Promise<ResidentObservation>
57
+
58
+ /** @experimental Host validates content and destination; null means no communication. */
59
+ export type ResidentMessageFactory = (
60
+ pursuit: ResidentPursuit,
61
+ decision: ResidentDecision,
62
+ signal: AbortSignal,
63
+ ) => Promise<ResidentMessageInput | null>
64
+
65
+ /** @experimental Opt-in local selection, observed settlement and atomic outbound intent. */
66
+ export interface ResidentHostOptions {
67
+ readonly select?: ResidentSelector
68
+ readonly observe?: ResidentObserver
69
+ readonly prepareMessage?: ResidentMessageFactory
70
+ /** Bind approved learning to the exact admission snapshot. Default false. */
71
+ readonly learning?: boolean
72
+ }
73
+
74
+ /**
75
+ * @experimental Local driver for one durable agenda. Shared agenda admission
76
+ * prevents overlapping pursuits across processes. Notifications are local;
77
+ * opt-in keepAlive also rereads durable state on bounded idle timers.
78
+ */
79
+ export class ResidentHost {
80
+ private controller: AbortController | undefined
81
+ private active: Promise<ResidentHostResult> | undefined
82
+ private idle: AbortController | undefined
83
+ private generation = 0
84
+ private controls: Promise<void> = Promise.resolve()
85
+ private controlsPending = 0
86
+ private readonly options: ResidentHostOptions
87
+
88
+ constructor(
89
+ private readonly agenda: ResidentAgendaStore,
90
+ private readonly step: ResidentContextualStep,
91
+ options: ResidentHostOptions = {},
92
+ ) {
93
+ if (options.learning !== undefined && typeof options.learning !== 'boolean')
94
+ throw new TypeError('Resident learning must be explicitly enabled with a boolean.')
95
+ if ((options.select || options.learning) && !agenda.executionAt)
96
+ throw new TypeError('Resident selection or learning requires atomic executionAt support.')
97
+ if (options.observe && !agenda.settleObserved)
98
+ throw new TypeError('Resident observation requires atomic settleObserved support.')
99
+ if (options.prepareMessage && !agenda.settleWithMessage)
100
+ throw new TypeError('Resident messages require atomic settleWithMessage support.')
101
+ this.options = Object.freeze({ ...options })
102
+ }
103
+
104
+ /** Interrupt local idle waiting after the host learns of new durable state. */
105
+ notify(): void {
106
+ this.generation++
107
+ this.idle?.abort()
108
+ }
109
+
110
+ private async snapshot(): Promise<ResidentAgendaState> {
111
+ const state = await this.agenda.read()
112
+ if (!state) throw new Error('Create the resident agenda before running the host.')
113
+ return state
114
+ }
115
+
116
+ private async paused(value: boolean): Promise<void> {
117
+ for (let attempt = 0; attempt < 8; attempt++) {
118
+ const state = await this.snapshot()
119
+ if (state.paused === value) return
120
+ try {
121
+ await this.agenda.setPaused(state, value)
122
+ return
123
+ } catch (error) {
124
+ if (!(error instanceof ResidentConflictError) || attempt === 7) throw error
125
+ }
126
+ }
127
+ }
128
+
129
+ private control(update: () => Promise<void>): Promise<void> {
130
+ this.controlsPending++
131
+ const pending = this.controls.then(update)
132
+ this.controls = pending.catch(() => {})
133
+ return pending.finally(() => {
134
+ this.controlsPending--
135
+ })
136
+ }
137
+
138
+ /**
139
+ * Signal current work and durably close future admission. Resolving this method
140
+ * is NOT quiescence: await the run promise before claiming the callback stopped.
141
+ * An interrupted admitted pursuit remains unresolved until reconciled explicitly.
142
+ */
143
+ async pause(): Promise<void> {
144
+ this.controller?.abort()
145
+ this.notify()
146
+ await this.control(() => this.paused(true))
147
+ }
148
+
149
+ /** Reauthorize future work only after this host's previous invocation drained. */
150
+ async resume(): Promise<void> {
151
+ if (this.controlsPending) throw new Error('Wait for pending resident controls before resuming.')
152
+ if (this.active)
153
+ throw new Error('Wait for the active resident invocation to drain before resuming.')
154
+ await this.control(() => this.paused(false))
155
+ this.notify()
156
+ }
157
+
158
+ /** Persist fresh evidence and interrupt this host's idle timer. */
159
+ async wake(id: string, reason: string): Promise<void> {
160
+ const state = await this.snapshot()
161
+ const pursuit = state.pursuits.find((p) => p.id === id)
162
+ if (!pursuit) throw new Error('Unknown resident pursuit.')
163
+ await this.agenda.wake(id, pursuit.state, reason, Date.now())
164
+ this.notify()
165
+ }
166
+
167
+ run(options: ResidentHostRunOptions): Promise<ResidentHostResult> {
168
+ if (this.controlsPending) throw new Error('Wait for pending resident controls before running.')
169
+ if (this.active) throw new Error('A resident host invocation is already active.')
170
+ const maxIdleMs = options.maxIdleMs ?? 60_000
171
+ const keepAlive = options.keepAlive ?? false
172
+ if (options.keepAlive !== undefined && typeof options.keepAlive !== 'boolean')
173
+ throw new TypeError('Resident keepAlive must be explicitly enabled with a boolean.')
174
+ if (
175
+ !Number.isSafeInteger(options.maxSteps) ||
176
+ options.maxSteps < 1 ||
177
+ !Number.isSafeInteger(maxIdleMs) ||
178
+ maxIdleMs < 0 ||
179
+ maxIdleMs > 2_147_483_647 ||
180
+ (keepAlive && maxIdleMs === 0)
181
+ )
182
+ throw new TypeError('Invalid resident host limits.')
183
+ const controller = new AbortController()
184
+ this.controller = controller
185
+ const signal = AbortSignal.any([controller.signal, options.signal])
186
+ this.active = this.drive(options.maxSteps, maxIdleMs, keepAlive, signal).finally(() => {
187
+ this.active = undefined
188
+ this.controller = undefined
189
+ this.idle = undefined
190
+ })
191
+ return this.active
192
+ }
193
+
194
+ private async drive(
195
+ maxSteps: number,
196
+ maxIdleMs: number,
197
+ keepAlive: boolean,
198
+ signal: AbortSignal,
199
+ ): Promise<ResidentHostResult> {
200
+ let stepsSettled = 0
201
+ try {
202
+ while (true) {
203
+ signal.throwIfAborted()
204
+ const observed = this.generation
205
+ const state = await this.snapshot()
206
+ signal.throwIfAborted()
207
+ if (state.paused) return { status: 'paused', stepsSettled, nextWakeAt: null }
208
+ if (state.pursuits.some((p) => p.state.phase === 'running'))
209
+ return { status: 'unresolved', stepsSettled, nextWakeAt: null }
210
+ const waiting = state.pursuits.filter(
211
+ (p) => p.state.phase === 'waiting' && p.state.wakeAt !== null,
212
+ )
213
+ const now = Date.now()
214
+ // Fair, deterministic baseline; this is not model-authored initiative.
215
+ let due = waiting
216
+ .filter((p) => (p.state.wakeAt ?? 0) <= now)
217
+ .sort(
218
+ (a, b) =>
219
+ a.state.stepsAdmitted - b.state.stepsAdmitted ||
220
+ (a.state.wakeAt ?? 0) - (b.state.wakeAt ?? 0) ||
221
+ a.id.localeCompare(b.id),
222
+ )[0]
223
+ let selection: ResidentSelection | undefined
224
+ if (due && this.options.select) {
225
+ selection = this.options.select(state, now)
226
+ signal.throwIfAborted()
227
+ if (selection.agendaRevision !== state.revision)
228
+ throw new Error('Resident selector returned a stale agenda revision.')
229
+ if (selection.pursuitId === null) due = undefined
230
+ else {
231
+ const selectedId = selection.pursuitId
232
+ due = waiting.find((p) => p.id === selectedId && (p.state.wakeAt ?? 0) <= now)
233
+ if (!due) throw new Error('Resident selector chose an ineligible pursuit.')
234
+ }
235
+ }
236
+
237
+ if (due) {
238
+ const selected = due
239
+ const context: ResidentStepContext = Object.freeze({
240
+ agendaRevision: state.revision,
241
+ ...(this.options.learning && state.learning
242
+ ? { learning: freezeResidentLearning(state.learning) }
243
+ : {}),
244
+ })
245
+ const execution =
246
+ this.options.select || this.options.learning
247
+ ? this.agenda.executionAt?.(selected.id, state)
248
+ : this.agenda.execution(selected.id)
249
+ if (!execution) throw new Error('Resident executionAt support was removed.')
250
+ const observe = this.options.observe
251
+ const prepareMessage = this.options.prepareMessage
252
+ const observedExecution =
253
+ observe || prepareMessage
254
+ ? {
255
+ read: () => execution.read(),
256
+ claim: (...args: Parameters<typeof execution.claim>) => execution.claim(...args),
257
+ settle: async (
258
+ current: Parameters<typeof execution.settle>[0],
259
+ decision: ResidentDecision,
260
+ at: number,
261
+ ) => {
262
+ const disposition = Object.freeze(residentDecisionSchema.parse(decision))
263
+ const observed = await observe?.(
264
+ { ...selected, state: current },
265
+ disposition,
266
+ signal,
267
+ )
268
+ signal.throwIfAborted()
269
+ const observation = observe
270
+ ? Object.freeze(residentObservationSchema.parse(observed))
271
+ : undefined
272
+ const prepared = await prepareMessage?.(
273
+ { ...selected, state: current },
274
+ disposition,
275
+ signal,
276
+ )
277
+ signal.throwIfAborted()
278
+ const message =
279
+ prepareMessage && prepared !== null
280
+ ? Object.freeze(residentMessageInputSchema.parse(prepared))
281
+ : null
282
+ if (message !== null) {
283
+ if (!this.agenda.settleWithMessage)
284
+ throw new Error('Resident settleWithMessage support was removed.')
285
+ return this.agenda.settleWithMessage(
286
+ selected.id,
287
+ current,
288
+ disposition,
289
+ message,
290
+ at,
291
+ observation,
292
+ )
293
+ }
294
+ if (observation) {
295
+ if (!this.agenda.settleObserved)
296
+ throw new Error('Resident settleObserved support was removed.')
297
+ return this.agenda.settleObserved(
298
+ selected.id,
299
+ current,
300
+ disposition,
301
+ observation,
302
+ at,
303
+ )
304
+ }
305
+ return execution.settle(current, disposition, at)
306
+ },
307
+ }
308
+ : execution
309
+ const result = await stepResident(
310
+ observedExecution,
311
+ (current, abort) => this.step({ ...selected, state: current }, abort, context),
312
+ signal,
313
+ )
314
+ if (result.status === 'idle')
315
+ return {
316
+ status: result.reason === 'unresolved' ? 'unresolved' : 'contended',
317
+ stepsSettled,
318
+ nextWakeAt: null,
319
+ }
320
+ stepsSettled++
321
+ if (stepsSettled >= maxSteps) return { status: 'limit', stepsSettled, nextWakeAt: null }
322
+ continue
323
+ }
324
+ const scheduled =
325
+ selection?.pursuitId === null
326
+ ? waiting.filter((p) => (p.state.wakeAt ?? 0) > now)
327
+ : waiting
328
+ const nextWakeAt =
329
+ scheduled.length === 0 ? null : Math.min(...scheduled.map((p) => p.state.wakeAt ?? now))
330
+ if (observed !== this.generation) continue
331
+ if (!keepAlive && (nextWakeAt === null || nextWakeAt - now > maxIdleMs))
332
+ return { status: 'idle', stepsSettled, nextWakeAt, ...(selection ? { selection } : {}) }
333
+ const delay =
334
+ nextWakeAt === null
335
+ ? maxIdleMs
336
+ : keepAlive
337
+ ? Math.min(maxIdleMs, nextWakeAt - now)
338
+ : nextWakeAt - now
339
+ const idle = new AbortController()
340
+ this.idle = idle
341
+ try {
342
+ await sleep(Math.max(1, delay), undefined, {
343
+ signal: AbortSignal.any([signal, idle.signal]),
344
+ })
345
+ } catch (error) {
346
+ if (!idle.signal.aborted || signal.aborted) throw error
347
+ } finally {
348
+ if (this.idle === idle) this.idle = undefined
349
+ }
350
+ }
351
+ } catch (error) {
352
+ if (!signal.aborted) throw error
353
+ return { status: 'cancelled', stepsSettled, nextWakeAt: null }
354
+ }
355
+ }
356
+ }
@@ -0,0 +1,195 @@
1
+ import { z } from 'zod'
2
+ import type { ResidentAgendaState } from './agenda.js'
3
+
4
+ const boundedText = z.string().trim().min(1).max(256)
5
+ const cost = z.number().finite().nonnegative()
6
+ export const residentObservationSchema = z.object({
7
+ evidenceKey: boundedText,
8
+ source: boundedText,
9
+ progress: z.number().finite().min(0).max(1),
10
+ /** All observations and policy costs use the same host-defined resource unit. */
11
+ costUnits: cost.nullable(),
12
+ })
13
+
14
+ /** @experimental Host-validated outcome and measured resource use, never model self-confidence. */
15
+ export type ResidentObservation = Readonly<z.infer<typeof residentObservationSchema>>
16
+
17
+ export const residentFeedbackSchema = z.object({
18
+ bestProgress: z.number().finite().min(0).max(1),
19
+ hasUnobservedSteps: z.boolean().default(false),
20
+ stagnantSteps: z.number().int().nonnegative().safe(),
21
+ observations: z
22
+ .array(
23
+ residentObservationSchema.extend({
24
+ gain: z.number().finite().min(0).max(1),
25
+ step: z.number().int().positive().safe(),
26
+ }),
27
+ )
28
+ .min(1)
29
+ .max(8),
30
+ })
31
+
32
+ /** @experimental Last eight observations; best progress and stagnation survive eviction. */
33
+ export type ResidentFeedback = Readonly<
34
+ Omit<z.infer<typeof residentFeedbackSchema>, 'observations'>
35
+ > & {
36
+ readonly observations: readonly Readonly<
37
+ z.infer<typeof residentFeedbackSchema>['observations'][number]
38
+ >[]
39
+ }
40
+
41
+ export function observeResidentStep(
42
+ previous: ResidentFeedback | undefined,
43
+ input: ResidentObservation,
44
+ step: number,
45
+ ): ResidentFeedback {
46
+ const observation = residentObservationSchema.parse(input)
47
+ const best = previous?.bestProgress ?? 0
48
+ const duplicate = previous?.observations.some(
49
+ (item) => item.evidenceKey === observation.evidenceKey,
50
+ )
51
+ // Rewording evidence, regression and recovery to an old best earn no credit.
52
+ const gain = duplicate ? 0 : Math.max(0, observation.progress - best)
53
+ return freezeResidentFeedback(
54
+ residentFeedbackSchema.parse({
55
+ bestProgress: best + gain,
56
+ hasUnobservedSteps:
57
+ (previous?.hasUnobservedSteps ?? false) ||
58
+ step !== (previous?.observations.at(-1)?.step ?? 0) + 1,
59
+ stagnantSteps: gain > 0 ? 0 : (previous?.stagnantSteps ?? 0) + 1,
60
+ observations: [...(previous?.observations ?? []), { ...observation, gain, step }].slice(-8),
61
+ }),
62
+ )
63
+ }
64
+
65
+ export function freezeResidentFeedback(value: ResidentFeedback): ResidentFeedback {
66
+ return Object.freeze({
67
+ ...value,
68
+ observations: Object.freeze(value.observations.map((item) => Object.freeze({ ...item }))),
69
+ })
70
+ }
71
+
72
+ /** @experimental Policy units are chosen by the host; scores are heuristics, not probabilities. */
73
+ export interface ResidentSelectionConfig {
74
+ readonly progressValue: number
75
+ readonly initialExpectedProgress: number
76
+ readonly initialExpectedCost: number
77
+ readonly maxStagnantSteps?: number
78
+ }
79
+
80
+ /** @experimental Explainable inputs for a single selection; unknown cost is not zero. */
81
+ export interface ResidentCandidate {
82
+ readonly pursuitId: string
83
+ readonly reason:
84
+ | 'eligible'
85
+ | 'not-due'
86
+ | 'missing-observation'
87
+ | 'stalled'
88
+ | 'unknown-cost'
89
+ | 'nonpositive-value'
90
+ readonly expectedGain: number | null
91
+ readonly expectedCost: number | null
92
+ readonly score: number | null
93
+ }
94
+
95
+ /** @experimental Choice is bound to the whole agenda revision used to compute it. */
96
+ export interface ResidentSelection {
97
+ readonly agendaRevision: number
98
+ readonly pursuitId: string | null
99
+ readonly reason: 'selected' | 'paused' | 'unresolved' | 'no-useful-work'
100
+ readonly candidates: readonly ResidentCandidate[]
101
+ }
102
+
103
+ /** @experimental Pure local selection; perform inference inside budgeted steps, not here. */
104
+ export type ResidentSelector = (agenda: ResidentAgendaState, now: number) => ResidentSelection
105
+
106
+ /**
107
+ * @experimental Cost-aware continuation with an explicit abstention choice.
108
+ * This is a measured-progress heuristic, not Bayesian value of computation.
109
+ * A finite bootstrap allowance permits delayed payoffs but may stop too early.
110
+ */
111
+ export function createResidentSelector(config: ResidentSelectionConfig): ResidentSelector {
112
+ const policy = z
113
+ .object({
114
+ progressValue: z.number().finite().positive(),
115
+ initialExpectedProgress: z.number().finite().positive().max(1),
116
+ initialExpectedCost: cost,
117
+ maxStagnantSteps: z.number().int().min(1).max(32).default(2),
118
+ })
119
+ .parse(config)
120
+ return (agenda, now) => {
121
+ z.number().int().nonnegative().safe().parse(now)
122
+ const finish = (
123
+ pursuitId: string | null,
124
+ reason: ResidentSelection['reason'],
125
+ candidates: ResidentCandidate[],
126
+ ): ResidentSelection =>
127
+ Object.freeze({
128
+ agendaRevision: agenda.revision,
129
+ pursuitId,
130
+ reason,
131
+ candidates: Object.freeze(candidates.map((candidate) => Object.freeze(candidate))),
132
+ })
133
+ if (agenda.paused) return finish(null, 'paused', [])
134
+ if (agenda.pursuits.some((p) => p.state.phase === 'running'))
135
+ return finish(null, 'unresolved', [])
136
+ const candidates = agenda.pursuits.map((p): ResidentCandidate => {
137
+ const reject = (reason: ResidentCandidate['reason']): ResidentCandidate => ({
138
+ pursuitId: p.id,
139
+ reason,
140
+ expectedGain: null,
141
+ expectedCost: null,
142
+ score: null,
143
+ })
144
+ if (p.state.phase !== 'waiting' || p.state.wakeAt === null || p.state.wakeAt > now)
145
+ return reject('not-due')
146
+ if (
147
+ p.state.stepsAdmitted > 0 &&
148
+ (!p.feedback ||
149
+ p.feedback.hasUnobservedSteps ||
150
+ p.feedback.observations.at(-1)?.step !== p.state.stepsAdmitted)
151
+ )
152
+ return reject('missing-observation')
153
+ const feedback = p.feedback
154
+ if (feedback && feedback.stagnantSteps >= policy.maxStagnantSteps) return reject('stalled')
155
+ if (feedback?.observations.some((item) => item.costUnits === null))
156
+ return reject('unknown-cost')
157
+ const recent = feedback?.observations ?? []
158
+ const totalGain = recent.reduce((total, item) => total + item.gain, 0)
159
+ const bootstrap = p.state.stepsAdmitted < policy.maxStagnantSteps && totalGain === 0
160
+ const expectedGain =
161
+ recent.length === 0 || bootstrap
162
+ ? policy.initialExpectedProgress
163
+ : (totalGain + policy.initialExpectedProgress) / (recent.length + 1)
164
+ const expectedCost =
165
+ recent.length === 0
166
+ ? policy.initialExpectedCost
167
+ : recent.reduce((total, item) => total + (item.costUnits ?? 0), 0) / recent.length
168
+ const score = policy.progressValue * expectedGain - expectedCost
169
+ return {
170
+ pursuitId: p.id,
171
+ reason: score > 0 ? 'eligible' : 'nonpositive-value',
172
+ expectedGain,
173
+ expectedCost,
174
+ score,
175
+ }
176
+ })
177
+ const eligible = candidates.filter((candidate) => candidate.reason === 'eligible')
178
+ eligible.sort((a, b) => {
179
+ const left = agenda.pursuits.find((p) => p.id === a.pursuitId)
180
+ const right = agenda.pursuits.find((p) => p.id === b.pursuitId)
181
+ if (!left || !right) throw new Error('Resident candidate is missing.')
182
+ return (
183
+ (b.score ?? 0) - (a.score ?? 0) ||
184
+ left.state.stepsAdmitted - right.state.stepsAdmitted ||
185
+ (left.state.wakeAt ?? 0) - (right.state.wakeAt ?? 0) ||
186
+ a.pursuitId.localeCompare(b.pursuitId)
187
+ )
188
+ })
189
+ return finish(
190
+ eligible[0]?.pursuitId ?? null,
191
+ eligible.length ? 'selected' : 'no-useful-work',
192
+ candidates,
193
+ )
194
+ }
195
+ }