@namzu/sdk 60.0.0 → 60.2.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 +40 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +1 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +19 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/acp/update.d.ts.map +1 -1
- package/dist/bridge/acp/update.js +1 -0
- package/dist/bridge/acp/update.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +28 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/constants/continuation.d.ts +9 -0
- package/dist/constants/continuation.d.ts.map +1 -1
- package/dist/constants/continuation.js +9 -0
- package/dist/constants/continuation.js.map +1 -1
- package/dist/contracts/session/api.d.ts +8 -2
- package/dist/contracts/session/api.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +7 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +20 -0
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/peers/dir.d.ts.map +1 -1
- package/dist/peers/dir.js +3 -0
- package/dist/peers/dir.js.map +1 -1
- package/dist/provider/collect-chat-completion.d.ts.map +1 -1
- package/dist/provider/collect-chat-completion.js +4 -0
- package/dist/provider/collect-chat-completion.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +11 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/mock.d.ts +2 -0
- package/dist/provider/mock.d.ts.map +1 -1
- package/dist/provider/mock.js +16 -8
- package/dist/provider/mock.js.map +1 -1
- package/dist/provider/model-window-suffix.d.ts.map +1 -1
- package/dist/provider/model-window-suffix.js +1 -0
- package/dist/provider/model-window-suffix.js.map +1 -1
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +5 -0
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +17 -0
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +23 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +88 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +14 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +14 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/effort-clamp.d.ts +13 -0
- package/dist/runtime/query/iteration/effort-clamp.d.ts.map +1 -0
- package/dist/runtime/query/iteration/effort-clamp.js +42 -0
- package/dist/runtime/query/iteration/effort-clamp.js.map +1 -0
- package/dist/runtime/query/iteration/index.d.ts +34 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +199 -6
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +6 -0
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +6 -0
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +12 -2
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +6 -5
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/session/paths.d.ts +15 -22
- package/dist/session/paths.d.ts.map +1 -1
- package/dist/session/paths.js +19 -46
- package/dist/session/paths.js.map +1 -1
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +1 -1
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/markdown.d.ts.map +1 -1
- package/dist/store/memory/markdown.js +1 -1
- package/dist/store/memory/markdown.js.map +1 -1
- package/dist/store/memory/operation-lock.d.ts +20 -4
- package/dist/store/memory/operation-lock.d.ts.map +1 -1
- package/dist/store/memory/operation-lock.js +235 -58
- package/dist/store/memory/operation-lock.js.map +1 -1
- package/dist/store/session-log/core.d.ts +43 -0
- package/dist/store/session-log/core.d.ts.map +1 -1
- package/dist/store/session-log/core.js +190 -3
- package/dist/store/session-log/core.js.map +1 -1
- package/dist/store/session-log/disk.d.ts +1 -1
- package/dist/store/session-log/disk.d.ts.map +1 -1
- package/dist/store/session-log/disk.js +1 -0
- package/dist/store/session-log/disk.js.map +1 -1
- package/dist/store/session-log/index.d.ts +1 -1
- package/dist/store/session-log/index.d.ts.map +1 -1
- package/dist/store/session-log/index.js +1 -1
- package/dist/store/session-log/index.js.map +1 -1
- package/dist/tools/builtins/bash-cwd.d.ts.map +1 -1
- package/dist/tools/builtins/bash-cwd.js +2 -3
- package/dist/tools/builtins/bash-cwd.js.map +1 -1
- package/dist/tools/builtins/read-media.js +3 -3
- package/dist/tools/builtins/read-media.js.map +1 -1
- package/dist/turn/reporter.d.ts.map +1 -1
- package/dist/turn/reporter.js +13 -0
- package/dist/turn/reporter.js.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +8 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +1 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/provider/chat.d.ts +19 -0
- package/dist/types/provider/chat.d.ts.map +1 -1
- package/dist/types/provider/config.d.ts +7 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/provider/index.d.ts +1 -1
- package/dist/types/provider/index.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +15 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/provider/stream.d.ts +6 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +72 -2
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js +4 -0
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +34 -34
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +5 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/stop-reason.d.ts +11 -0
- package/dist/types/session/stop-reason.d.ts.map +1 -1
- package/dist/utils/temp.d.ts +82 -0
- package/dist/utils/temp.d.ts.map +1 -0
- package/dist/utils/temp.js +196 -0
- package/dist/utils/temp.js.map +1 -0
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +1 -0
- package/src/bridge/a2a/mapper.ts +22 -0
- package/src/bridge/acp/update.ts +6 -1
- package/src/bridge/sse/mapper.ts +29 -0
- package/src/constants/continuation.ts +13 -0
- package/src/contracts/session/api.ts +8 -0
- package/src/manager/session/turn-recorder.ts +22 -0
- package/src/peers/dir.ts +3 -0
- package/src/provider/collect-chat-completion.ts +3 -0
- package/src/provider/fallback.ts +17 -0
- package/src/provider/idle-timeout.ts +9 -0
- package/src/provider/mock.ts +19 -9
- package/src/provider/model-window-suffix.ts +1 -0
- package/src/provider/retry.ts +9 -0
- package/src/public-runtime.ts +13 -0
- package/src/runtime/query/events.ts +19 -0
- package/src/runtime/query/executor.ts +92 -8
- package/src/runtime/query/index.ts +27 -0
- package/src/runtime/query/iteration/effort-clamp.ts +46 -0
- package/src/runtime/query/iteration/index.ts +260 -6
- package/src/runtime/query/iteration/phases/context.ts +2 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +6 -0
- package/src/runtime/query/iteration/stream-turn.ts +5 -0
- package/src/runtime/query/review-policy.ts +13 -2
- package/src/runtime/query/tooling.ts +3 -0
- package/src/sandbox/provider/local.ts +8 -5
- package/src/session/paths.ts +32 -57
- package/src/store/memory/disk.ts +1 -0
- package/src/store/memory/markdown.ts +1 -0
- package/src/store/memory/operation-lock.ts +288 -57
- package/src/store/session-log/core.ts +235 -3
- package/src/store/session-log/disk.ts +7 -1
- package/src/store/session-log/index.ts +4 -0
- package/src/tools/builtins/bash-cwd.ts +2 -3
- package/src/tools/builtins/read-media.ts +3 -3
- package/src/turn/reporter.ts +14 -0
- package/src/types/agent/query.ts +2 -0
- package/src/types/hitl/index.ts +8 -0
- package/src/types/message/index.ts +5 -3
- package/src/types/provider/chat.ts +20 -0
- package/src/types/provider/config.ts +7 -0
- package/src/types/provider/index.ts +1 -0
- package/src/types/provider/interface.ts +20 -0
- package/src/types/provider/stream.ts +6 -0
- package/src/types/session/events.ts +106 -6
- package/src/types/session/records.ts +5 -0
- package/src/types/session/stop-reason.ts +11 -0
- package/src/utils/temp.ts +250 -0
|
@@ -102,7 +102,12 @@ type CoreSessionEvent =
|
|
|
102
102
|
/** The token ledger this turn spends from, keyed by the root turn. */
|
|
103
103
|
budget?: TurnBudgetBinding
|
|
104
104
|
}
|
|
105
|
-
| {
|
|
105
|
+
| {
|
|
106
|
+
type: 'iteration_started'
|
|
107
|
+
sessionId: SessionId
|
|
108
|
+
turnId: TurnId
|
|
109
|
+
iteration: number
|
|
110
|
+
}
|
|
106
111
|
/**
|
|
107
112
|
* Who answers when this turn asks a human, changed mid-turn.
|
|
108
113
|
*
|
|
@@ -206,6 +211,40 @@ type CoreSessionEvent =
|
|
|
206
211
|
/** The window the trigger measured against. */
|
|
207
212
|
contextWindowTokens: number
|
|
208
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* The session log can no longer write: the volume holding it is full, over
|
|
216
|
+
* its quota or read-only. The session goes on in memory and the records not
|
|
217
|
+
* yet written wait in a bounded buffer, so a reply is not lost, but a
|
|
218
|
+
* restart before the log recovers would resume from an older point. Emitted
|
|
219
|
+
* once on entering that state, and once more with `dropping` when the
|
|
220
|
+
* buffer first overflowed and its oldest records were discarded.
|
|
221
|
+
*
|
|
222
|
+
* Live only, never recorded: the log is what cannot be written.
|
|
223
|
+
*/
|
|
224
|
+
| {
|
|
225
|
+
type: 'session_log_degraded'
|
|
226
|
+
sessionId: SessionId
|
|
227
|
+
turnId?: TurnId
|
|
228
|
+
cause: 'disk_full' | 'quota_exceeded' | 'read_only'
|
|
229
|
+
/** Records waiting to be written. */
|
|
230
|
+
bufferedRecords: number
|
|
231
|
+
bufferedBytes: number
|
|
232
|
+
/** The buffer overflowed and the oldest waiting records were discarded. */
|
|
233
|
+
dropping?: boolean
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The session log writes again after `session_log_degraded`: the waiting
|
|
237
|
+
* records were written, in order. `droppedRecords` is how many were
|
|
238
|
+
* discarded while the buffer was full; the log has a gap where they were.
|
|
239
|
+
* Live only, never recorded.
|
|
240
|
+
*/
|
|
241
|
+
| {
|
|
242
|
+
type: 'session_log_recovered'
|
|
243
|
+
sessionId: SessionId
|
|
244
|
+
turnId?: TurnId
|
|
245
|
+
flushedRecords: number
|
|
246
|
+
droppedRecords: number
|
|
247
|
+
}
|
|
209
248
|
| {
|
|
210
249
|
type: 'compaction_completed'
|
|
211
250
|
sessionId: SessionId
|
|
@@ -454,6 +493,40 @@ type CoreSessionEvent =
|
|
|
454
493
|
*/
|
|
455
494
|
restart?: boolean
|
|
456
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* The model declined to answer, and what the turn did about it.
|
|
498
|
+
*
|
|
499
|
+
* The provider finished a reply as a refusal or a safety block. The refused
|
|
500
|
+
* reply is kept out of the history sent next. `outcome` says what followed:
|
|
501
|
+
*
|
|
502
|
+
* - `'fallback'`: the request is being re-run on `fallbackModel`, which the
|
|
503
|
+
* rest of the turn keeps. A host that owns the session's model is expected
|
|
504
|
+
* to keep it for later turns too and to say so.
|
|
505
|
+
* - `'retry'`: no other model was available and the refused reply had
|
|
506
|
+
* partial output, so the request is being re-run once on the same model
|
|
507
|
+
* with a note that the previous reply was stopped.
|
|
508
|
+
* - `'declined'`: nothing recovered it. The turn settles with stop reason
|
|
509
|
+
* `'refused'`.
|
|
510
|
+
*
|
|
511
|
+
* Like `provider_fallback`, this is the feature's honesty: a swapped model
|
|
512
|
+
* must be shown, not logged. It is emitted before the re-run starts.
|
|
513
|
+
*/
|
|
514
|
+
| {
|
|
515
|
+
type: 'model_refusal'
|
|
516
|
+
sessionId: SessionId
|
|
517
|
+
turnId: TurnId
|
|
518
|
+
iteration: number
|
|
519
|
+
/** The model that declined. */
|
|
520
|
+
model: string
|
|
521
|
+
providerId: string
|
|
522
|
+
outcome: 'fallback' | 'retry' | 'declined'
|
|
523
|
+
/** The model the request moves to. Set only with `outcome: 'fallback'`. */
|
|
524
|
+
fallbackModel?: string
|
|
525
|
+
/** The provider's own classification, when it gave one. */
|
|
526
|
+
category?: string
|
|
527
|
+
/** The provider's own sentence, when it gave one. Show it; do not parse it. */
|
|
528
|
+
explanation?: string
|
|
529
|
+
}
|
|
457
530
|
| {
|
|
458
531
|
type: 'tool_completed'
|
|
459
532
|
/** Bounded result view; omitted when output was overridden or truncated. */
|
|
@@ -892,7 +965,12 @@ type CoreSessionEvent =
|
|
|
892
965
|
steps: PlanStep[]
|
|
893
966
|
summary?: string
|
|
894
967
|
}
|
|
895
|
-
| {
|
|
968
|
+
| {
|
|
969
|
+
type: 'plan_approved'
|
|
970
|
+
sessionId: SessionId
|
|
971
|
+
turnId: TurnId
|
|
972
|
+
planId: PlanId
|
|
973
|
+
}
|
|
896
974
|
| {
|
|
897
975
|
type: 'plan_rejected'
|
|
898
976
|
sessionId: SessionId
|
|
@@ -923,7 +1001,12 @@ type CoreSessionEvent =
|
|
|
923
1001
|
* through `onContextCreated`, so they proved the plan settled without ever
|
|
924
1002
|
* asking whether a consumer of the EVENT STREAM could see it.
|
|
925
1003
|
*/
|
|
926
|
-
| {
|
|
1004
|
+
| {
|
|
1005
|
+
type: 'plan_completed'
|
|
1006
|
+
sessionId: SessionId
|
|
1007
|
+
turnId: TurnId
|
|
1008
|
+
planId: PlanId
|
|
1009
|
+
}
|
|
927
1010
|
/**
|
|
928
1011
|
* The plan is over and it did not finish.
|
|
929
1012
|
*
|
|
@@ -931,7 +1014,13 @@ type CoreSessionEvent =
|
|
|
931
1014
|
* an event that says "failed" without saying why puts the reader back
|
|
932
1015
|
* where the missing event did.
|
|
933
1016
|
*/
|
|
934
|
-
| {
|
|
1017
|
+
| {
|
|
1018
|
+
type: 'plan_failed'
|
|
1019
|
+
sessionId: SessionId
|
|
1020
|
+
turnId: TurnId
|
|
1021
|
+
planId: PlanId
|
|
1022
|
+
reason?: string
|
|
1023
|
+
}
|
|
935
1024
|
| {
|
|
936
1025
|
type: 'agent_pending'
|
|
937
1026
|
sessionId: SessionId
|
|
@@ -1103,7 +1192,12 @@ type CoreSessionEvent =
|
|
|
1103
1192
|
exitCode: number
|
|
1104
1193
|
durationMs: number
|
|
1105
1194
|
}
|
|
1106
|
-
| {
|
|
1195
|
+
| {
|
|
1196
|
+
type: 'sandbox_destroyed'
|
|
1197
|
+
sessionId: SessionId
|
|
1198
|
+
turnId?: TurnId
|
|
1199
|
+
sandboxId: SandboxId
|
|
1200
|
+
}
|
|
1107
1201
|
// ─────────────────────────────────────────────────────────────────────
|
|
1108
1202
|
// v3 message + tool-input lifecycle (additive 2026-05; see
|
|
1109
1203
|
// ses_001-tool-stream-events). These are not yet emitted by the
|
|
@@ -1310,6 +1404,8 @@ export type SessionEventListener = (event: SessionEvent) => void | Promise<void>
|
|
|
1310
1404
|
* runtime-context message or tool result that carried it, and a noisy watch
|
|
1311
1405
|
* would write tens of megabytes an hour of raw process output a second time.
|
|
1312
1406
|
* `background_job_exited.monitor` is the durable record of how a watch ended.
|
|
1407
|
+
* `session_log_degraded` and `session_log_recovered` are ephemeral because the
|
|
1408
|
+
* log is what cannot be written while they matter.
|
|
1313
1409
|
* `compaction_started` is ephemeral too: it only says a pass is about to run,
|
|
1314
1410
|
* and the `compaction` record or `compaction_failed` that ends it is the
|
|
1315
1411
|
* durable fact.
|
|
@@ -1321,13 +1417,15 @@ export const EPHEMERAL_EVENT_TYPES: ReadonlySet<SessionEventType> = new Set<Sess
|
|
|
1321
1417
|
'tool_progress',
|
|
1322
1418
|
'monitor_event',
|
|
1323
1419
|
'compaction_started',
|
|
1420
|
+
'session_log_degraded',
|
|
1421
|
+
'session_log_recovered',
|
|
1324
1422
|
])
|
|
1325
1423
|
|
|
1326
1424
|
export function isEphemeralEvent(event: { readonly type: string }): boolean {
|
|
1327
1425
|
return EPHEMERAL_EVENT_TYPES.has(event.type as SessionEventType)
|
|
1328
1426
|
}
|
|
1329
1427
|
|
|
1330
|
-
/** The event types a session log records: every literal except the ephemeral
|
|
1428
|
+
/** The event types a session log records: every literal except the ephemeral eight. */
|
|
1331
1429
|
export type PersistedSessionEventType = Exclude<
|
|
1332
1430
|
SessionEventType,
|
|
1333
1431
|
| 'text_delta'
|
|
@@ -1336,4 +1434,6 @@ export type PersistedSessionEventType = Exclude<
|
|
|
1336
1434
|
| 'tool_progress'
|
|
1337
1435
|
| 'monitor_event'
|
|
1338
1436
|
| 'compaction_started'
|
|
1437
|
+
| 'session_log_degraded'
|
|
1438
|
+
| 'session_log_recovered'
|
|
1339
1439
|
>
|
|
@@ -236,6 +236,7 @@ const stopReason = z.enum([
|
|
|
236
236
|
'step_refused',
|
|
237
237
|
'structured_output_failed',
|
|
238
238
|
'output_limit',
|
|
239
|
+
'refused',
|
|
239
240
|
'answer_rejected',
|
|
240
241
|
'input_guardrail',
|
|
241
242
|
'output_guardrail',
|
|
@@ -646,6 +647,8 @@ const SESSION_EVENT_TYPE_TABLE: Record<SessionEventType, true> = {
|
|
|
646
647
|
request_envelope: true,
|
|
647
648
|
iteration_completed: true,
|
|
648
649
|
compaction_started: true,
|
|
650
|
+
session_log_degraded: true,
|
|
651
|
+
session_log_recovered: true,
|
|
649
652
|
compaction_shed: true,
|
|
650
653
|
compaction_completed: true,
|
|
651
654
|
compaction_tool_results_cleared: true,
|
|
@@ -655,6 +658,7 @@ const SESSION_EVENT_TYPE_TABLE: Record<SessionEventType, true> = {
|
|
|
655
658
|
hosted_tool: true,
|
|
656
659
|
provider_retry: true,
|
|
657
660
|
provider_fallback: true,
|
|
661
|
+
model_refusal: true,
|
|
658
662
|
tool_completed: true,
|
|
659
663
|
user_question_asked: true,
|
|
660
664
|
user_question_answered: true,
|
|
@@ -749,6 +753,7 @@ const TURN_BOUND_OTHER_TYPE_LIST = [
|
|
|
749
753
|
'hosted_tool',
|
|
750
754
|
'provider_retry',
|
|
751
755
|
'provider_fallback',
|
|
756
|
+
'model_refusal',
|
|
752
757
|
'tool_completed',
|
|
753
758
|
'user_question_asked',
|
|
754
759
|
'user_question_answered',
|
|
@@ -53,6 +53,17 @@ export type StopReason =
|
|
|
53
53
|
* looking for a loop instead of at the reviewer.
|
|
54
54
|
*/
|
|
55
55
|
| 'answer_rejected'
|
|
56
|
+
/**
|
|
57
|
+
* The model declined to answer and nothing recovered it.
|
|
58
|
+
*
|
|
59
|
+
* Its provider finished the reply as a refusal or a safety block, and the
|
|
60
|
+
* one automatic retry this turn allows was not available or was refused
|
|
61
|
+
* too. Distinct from `end_turn` so a host never reads a declined request
|
|
62
|
+
* as a finished one, and from `error` because nothing failed: the model
|
|
63
|
+
* answered, and the answer was no. The refused reply is not in the
|
|
64
|
+
* history; a `model_refusal` event carries what the provider said.
|
|
65
|
+
*/
|
|
66
|
+
| 'refused'
|
|
56
67
|
/** An input guardrail refused the turn before it started. */
|
|
57
68
|
| 'input_guardrail'
|
|
58
69
|
/** An output guardrail refused the produced result. */
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { lstatSync, mkdirSync, mkdtempSync } from 'node:fs'
|
|
3
|
+
import { mkdtemp } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir, userInfo } from 'node:os'
|
|
5
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Namzu's own scratch space: the one place that decides where temporary files
|
|
9
|
+
* go, who may enter, and what a failure to create it says.
|
|
10
|
+
*
|
|
11
|
+
* The base is `NAMZU_TMPDIR` when it is set, otherwise the operating system's
|
|
12
|
+
* temporary directory. Under it, `namzu-<user>` is private to the user (mode
|
|
13
|
+
* 0700, refused when it is a symbolic link, not a directory, owned by someone
|
|
14
|
+
* else, or open to group and others). A failure is never answered by quietly
|
|
15
|
+
* using another directory: it is a {@link TempDirectoryError} that names the
|
|
16
|
+
* directory, the cause and `NAMZU_TMPDIR`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** The environment variable that moves Namzu's scratch space to another folder or disk. */
|
|
20
|
+
export const NAMZU_TMPDIR_ENV = 'NAMZU_TMPDIR'
|
|
21
|
+
|
|
22
|
+
export type TempDirectoryFailure =
|
|
23
|
+
| 'volume_full'
|
|
24
|
+
| 'quota_exceeded'
|
|
25
|
+
| 'not_writable'
|
|
26
|
+
| 'missing'
|
|
27
|
+
| 'not_absolute'
|
|
28
|
+
| 'refused'
|
|
29
|
+
|
|
30
|
+
/** Namzu's scratch directory cannot be used. `message` is a sentence for a person. */
|
|
31
|
+
export class TempDirectoryError extends Error {
|
|
32
|
+
override readonly name = 'TempDirectoryError'
|
|
33
|
+
constructor(
|
|
34
|
+
message: string,
|
|
35
|
+
readonly reason: TempDirectoryFailure,
|
|
36
|
+
readonly directory: string,
|
|
37
|
+
) {
|
|
38
|
+
super(message)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PrivateTempRootOptions {
|
|
43
|
+
/** Base directory. Default `NAMZU_TMPDIR`, else `os.tmpdir()`. */
|
|
44
|
+
readonly tmpdir?: string
|
|
45
|
+
/** Default `process.env`. */
|
|
46
|
+
readonly env?: NodeJS.ProcessEnv
|
|
47
|
+
/** Default `process.platform`. */
|
|
48
|
+
readonly platform?: NodeJS.Platform
|
|
49
|
+
/**
|
|
50
|
+
* Default `process.getuid`. Pass `undefined` explicitly to model a
|
|
51
|
+
* platform without it (win32).
|
|
52
|
+
*/
|
|
53
|
+
readonly getuid?: (() => number) | undefined
|
|
54
|
+
/** Default `os.userInfo().username`. */
|
|
55
|
+
readonly username?: () => string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The directory under which Namzu keeps its temporary files: `NAMZU_TMPDIR`
|
|
60
|
+
* when set (it must be an absolute path), otherwise `os.tmpdir()`. Nothing is
|
|
61
|
+
* created or checked.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveTempBase(env: NodeJS.ProcessEnv = process.env): string {
|
|
64
|
+
const configured = env[NAMZU_TMPDIR_ENV]?.trim()
|
|
65
|
+
if (!configured) return tmpdir()
|
|
66
|
+
if (!isAbsolute(configured)) {
|
|
67
|
+
throw new TempDirectoryError(
|
|
68
|
+
`${NAMZU_TMPDIR_ENV} must be a full path, but it is "${configured}". Set it to a folder on a disk with free space.`,
|
|
69
|
+
'not_absolute',
|
|
70
|
+
configured,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return configured
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The sentence for a directory that could not be created or written. */
|
|
77
|
+
function explain(error: unknown, directory: string): TempDirectoryError | undefined {
|
|
78
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code
|
|
79
|
+
const elsewhere = `Free some space and start again, or set ${NAMZU_TMPDIR_ENV} to a folder on another disk.`
|
|
80
|
+
switch (code) {
|
|
81
|
+
case 'ENOSPC':
|
|
82
|
+
return new TempDirectoryError(
|
|
83
|
+
`The disk is full, so Namzu could not create ${directory}. ${elsewhere}`,
|
|
84
|
+
'volume_full',
|
|
85
|
+
directory,
|
|
86
|
+
)
|
|
87
|
+
case 'EDQUOT':
|
|
88
|
+
return new TempDirectoryError(
|
|
89
|
+
`Your disk quota is used up, so Namzu could not create ${directory}. ${elsewhere}`,
|
|
90
|
+
'quota_exceeded',
|
|
91
|
+
directory,
|
|
92
|
+
)
|
|
93
|
+
case 'EACCES':
|
|
94
|
+
case 'EPERM':
|
|
95
|
+
case 'EROFS':
|
|
96
|
+
return new TempDirectoryError(
|
|
97
|
+
`Namzu could not create ${directory} because the folder cannot be written to. Set ${NAMZU_TMPDIR_ENV} to a folder you can write to.`,
|
|
98
|
+
'not_writable',
|
|
99
|
+
directory,
|
|
100
|
+
)
|
|
101
|
+
case 'ENOENT':
|
|
102
|
+
return new TempDirectoryError(
|
|
103
|
+
`Namzu could not create ${directory} because its parent folder does not exist. Create that folder, or set ${NAMZU_TMPDIR_ENV} to one that does.`,
|
|
104
|
+
'missing',
|
|
105
|
+
directory,
|
|
106
|
+
)
|
|
107
|
+
default:
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function refuse(root: string, why: string): TempDirectoryError {
|
|
113
|
+
return new TempDirectoryError(
|
|
114
|
+
`Refusing temporary root ${root}: ${why} Set ${NAMZU_TMPDIR_ENV} to a folder only you can write to.`,
|
|
115
|
+
'refused',
|
|
116
|
+
root,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sha256Hex(value: string): string {
|
|
121
|
+
return createHash('sha256').update(value).digest('hex')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The per-user private root, `<base>/namzu-<user>`, created if missing.
|
|
126
|
+
*
|
|
127
|
+
* `<user>` is the numeric uid where the platform has one; otherwise the first
|
|
128
|
+
* 12 hex digits of the SHA-256 of the user name; otherwise `user`. On POSIX
|
|
129
|
+
* the directory is created with mode 0700 and refused if it is a symlink, not
|
|
130
|
+
* a directory, owned by another uid, or open to group or others. On win32
|
|
131
|
+
* `%TEMP%` is already per user: a symlink or junction is refused and the
|
|
132
|
+
* owner check is skipped.
|
|
133
|
+
*
|
|
134
|
+
* @throws {TempDirectoryError} when it cannot be created or is not safe to use.
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* Where {@link privateTempRoot} lives, without creating or checking anything.
|
|
138
|
+
* For a caller that needs to know a path before it is worth making it.
|
|
139
|
+
*/
|
|
140
|
+
export function privateTempRootPath(options: PrivateTempRootOptions = {}): string {
|
|
141
|
+
const getuid = 'getuid' in options ? options.getuid : process.getuid?.bind(process)
|
|
142
|
+
let user = 'user'
|
|
143
|
+
if (typeof getuid === 'function') {
|
|
144
|
+
user = String(getuid())
|
|
145
|
+
} else {
|
|
146
|
+
try {
|
|
147
|
+
const name = (options.username ?? (() => userInfo().username))()
|
|
148
|
+
if (name.length > 0) user = sha256Hex(name).slice(0, 12)
|
|
149
|
+
} catch {
|
|
150
|
+
// No user database entry: fall back to the fixed segment.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return join(options.tmpdir ?? resolveTempBase(options.env), `namzu-${user}`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function privateTempRoot(options: PrivateTempRootOptions = {}): string {
|
|
157
|
+
const platform = options.platform ?? process.platform
|
|
158
|
+
const getuid = 'getuid' in options ? options.getuid : process.getuid?.bind(process)
|
|
159
|
+
const root = privateTempRootPath(options)
|
|
160
|
+
try {
|
|
161
|
+
mkdirSync(root, { mode: 0o700 })
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw explain(error, root) ?? error
|
|
164
|
+
}
|
|
165
|
+
const entry = lstatSync(root)
|
|
166
|
+
if (entry.isSymbolicLink()) throw refuse(root, 'it is a symbolic link.')
|
|
167
|
+
if (!entry.isDirectory()) throw refuse(root, 'it is not a directory.')
|
|
168
|
+
if (platform !== 'win32' && typeof getuid === 'function') {
|
|
169
|
+
if (entry.uid !== getuid()) throw refuse(root, `it belongs to uid ${entry.uid}.`)
|
|
170
|
+
if ((entry.mode & 0o077) !== 0) {
|
|
171
|
+
throw refuse(
|
|
172
|
+
root,
|
|
173
|
+
`mode ${(entry.mode & 0o777).toString(8)} lets other users in; it must be 700.`,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return root
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A new empty directory named `<prefix>XXXXXX` inside the private root. */
|
|
181
|
+
export function createTempDirSync(prefix: string, options: PrivateTempRootOptions = {}): string {
|
|
182
|
+
const root = privateTempRoot(options)
|
|
183
|
+
try {
|
|
184
|
+
return mkdtempSync(join(root, prefix))
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw explain(error, join(root, prefix)) ?? error
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** As {@link createTempDirSync}, without blocking the event loop. */
|
|
191
|
+
export async function createTempDir(
|
|
192
|
+
prefix: string,
|
|
193
|
+
options: PrivateTempRootOptions = {},
|
|
194
|
+
): Promise<string> {
|
|
195
|
+
const root = privateTempRoot(options)
|
|
196
|
+
try {
|
|
197
|
+
return await mkdtemp(join(root, prefix))
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw explain(error, join(root, prefix)) ?? error
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The path of a file named `name` inside the private root. The file itself is
|
|
205
|
+
* not created; the caller opens it, and a write that fails because the disk is
|
|
206
|
+
* full is the caller's to report.
|
|
207
|
+
*/
|
|
208
|
+
export function tempFilePath(name: string, options: PrivateTempRootOptions = {}): string {
|
|
209
|
+
return join(privateTempRoot(options), name)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Make a session's scratch folder (and the folders above it, mode 0700) and
|
|
214
|
+
* confirm it is safe to treat as a place the session may write freely: it lies
|
|
215
|
+
* inside the private root, and neither it nor any folder between it and the
|
|
216
|
+
* root is a symbolic link, a non-folder or owned by someone else. Made on
|
|
217
|
+
* first use, not before, so a session that never needs it leaves nothing behind.
|
|
218
|
+
*
|
|
219
|
+
* @throws {TempDirectoryError} when it cannot be made or is not safe; the
|
|
220
|
+
* caller then leaves the folder outside the session's roots.
|
|
221
|
+
*/
|
|
222
|
+
export function ensureScratchpadDirectory(
|
|
223
|
+
directory: string,
|
|
224
|
+
options: PrivateTempRootOptions = {},
|
|
225
|
+
): string {
|
|
226
|
+
const root = privateTempRoot(options)
|
|
227
|
+
const target = resolve(directory)
|
|
228
|
+
const inside = relative(root, target)
|
|
229
|
+
if (inside === '' || inside.startsWith('..') || isAbsolute(inside)) {
|
|
230
|
+
throw refuse(target, 'it is not inside the private temporary folder.')
|
|
231
|
+
}
|
|
232
|
+
const getuid = 'getuid' in options ? options.getuid : process.getuid?.bind(process)
|
|
233
|
+
const platform = options.platform ?? process.platform
|
|
234
|
+
let current = root
|
|
235
|
+
for (const part of inside.split(sep)) {
|
|
236
|
+
current = join(current, part)
|
|
237
|
+
try {
|
|
238
|
+
mkdirSync(current, { mode: 0o700 })
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw explain(error, current) ?? error
|
|
241
|
+
}
|
|
242
|
+
const entry = lstatSync(current)
|
|
243
|
+
if (entry.isSymbolicLink()) throw refuse(current, 'it is a symbolic link.')
|
|
244
|
+
if (!entry.isDirectory()) throw refuse(current, 'it is not a directory.')
|
|
245
|
+
if (platform !== 'win32' && typeof getuid === 'function' && entry.uid !== getuid()) {
|
|
246
|
+
throw refuse(current, `it belongs to uid ${entry.uid}.`)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return target
|
|
250
|
+
}
|