orizu 0.5.39 → 0.5.40
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/README.md +77 -0
- package/dist/dataset-commands.d.ts +3 -1
- package/dist/dataset-commands.d.ts.map +1 -1
- package/dist/dataset-commands.js +101 -62
- package/dist/dataset-delete-workflow.d.ts +4 -0
- package/dist/dataset-delete-workflow.d.ts.map +1 -0
- package/dist/dataset-delete-workflow.js +75 -0
- package/dist/dataset-lock-workflow.d.ts +12 -0
- package/dist/dataset-lock-workflow.d.ts.map +1 -0
- package/dist/dataset-lock-workflow.js +76 -0
- package/dist/dataset-readme-workflow.d.ts +13 -0
- package/dist/dataset-readme-workflow.d.ts.map +1 -0
- package/dist/dataset-readme-workflow.js +82 -0
- package/dist/dataset-row-workflow.d.ts +14 -0
- package/dist/dataset-row-workflow.d.ts.map +1 -0
- package/dist/dataset-row-workflow.js +202 -0
- package/dist/dataset-split-reads.d.ts +28 -0
- package/dist/dataset-split-reads.d.ts.map +1 -0
- package/dist/dataset-split-reads.js +47 -0
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +29 -3
- package/dist/hosted-boot.d.ts +1 -0
- package/dist/hosted-boot.d.ts.map +1 -1
- package/dist/hosted-boot.js +17 -0
- package/dist/hosted-harness-opencode.d.ts +4 -5
- package/dist/hosted-harness-opencode.d.ts.map +1 -1
- package/dist/hosted-harness-opencode.js +66 -52
- package/dist/hosted-loop-lifecycle.d.ts +4 -10
- package/dist/hosted-loop-lifecycle.d.ts.map +1 -1
- package/dist/hosted-loop-lifecycle.js +4 -10
- package/dist/hosted-loop.d.ts +2 -0
- package/dist/hosted-loop.d.ts.map +1 -1
- package/dist/hosted-loop.js +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/instruction-delivery.d.ts +14 -0
- package/dist/instruction-delivery.d.ts.map +1 -0
- package/dist/instruction-delivery.js +103 -0
- package/dist/instruction-set-sync/index.d.ts +17 -0
- package/dist/instruction-set-sync/index.d.ts.map +1 -1
- package/dist/instruction-set-sync/index.js +44 -17
- package/dist/instruction-set-update/index.d.ts +2 -2
- package/dist/instruction-set-update/index.d.ts.map +1 -1
- package/dist/instruction-set-update/index.js +2 -2
- package/dist/instruction-sets-cli.d.ts.map +1 -1
- package/dist/instruction-sets-cli.js +19 -27
- package/dist/json-response.d.ts +1 -0
- package/dist/json-response.d.ts.map +1 -1
- package/dist/json-response.js +7 -0
- package/dist/preview-runtime.d.ts +2 -2
- package/dist/preview-runtime.d.ts.map +1 -1
- package/dist/preview-runtime.js +37 -7
- package/dist/task-export-workflow.d.ts +4 -0
- package/dist/task-export-workflow.d.ts.map +1 -0
- package/dist/task-export-workflow.js +46 -0
- package/dist/workbench-cli.js +1 -1
- package/package.json +1 -1
- package/src/dataset-commands.ts +92 -92
- package/src/dataset-delete-workflow.ts +72 -0
- package/src/dataset-lock-workflow.ts +74 -0
- package/src/dataset-readme-workflow.ts +80 -0
- package/src/dataset-row-workflow.ts +248 -0
- package/src/dataset-split-reads.ts +43 -0
- package/src/help.ts +29 -3
- package/src/hosted-boot.ts +16 -1
- package/src/hosted-harness-opencode.ts +67 -65
- package/src/hosted-loop-lifecycle.ts +4 -10
- package/src/hosted-loop.ts +6 -4
- package/src/hosted-model-cache.d.mts +2 -0
- package/src/hosted-model-cache.mjs +53 -0
- package/src/index.ts +10 -0
- package/src/instruction-delivery.ts +123 -0
- package/src/instruction-set-sync/index.ts +61 -14
- package/src/instruction-set-update/index.ts +4 -3
- package/src/instruction-sets-cli.ts +19 -27
- package/src/json-response.ts +8 -0
- package/src/preview-runtime.ts +40 -10
- package/src/task-export-workflow.ts +56 -0
- package/src/workbench-cli.ts +1 -1
- package/vendor/skills/orizu/references/cli-reference.md +34 -0
- package/vendor/skills/orizu/references/dataset-design.md +1 -1
- package/dist/dataset-edit-rows.d.ts +0 -10
- package/dist/dataset-edit-rows.d.ts.map +0 -1
- package/dist/dataset-edit-rows.js +0 -90
- package/src/dataset-edit-rows.ts +0 -124
|
@@ -51,8 +51,10 @@
|
|
|
51
51
|
|
|
52
52
|
import { readFileSync } from 'fs'
|
|
53
53
|
|
|
54
|
+
import { refreshHostedModels } from '../src/hosted-model-cache.mjs'
|
|
55
|
+
|
|
54
56
|
import { HOSTED_AGENT_NAME, HOSTED_AGENT_PROMPT } from './hosted-agent-prompt.js'
|
|
55
|
-
import {
|
|
57
|
+
import { parseModelIdentity } from './provider-registry.js'
|
|
56
58
|
|
|
57
59
|
import { HARNESS_NAMES } from './hosted-harness.js'
|
|
58
60
|
import type {
|
|
@@ -152,17 +154,6 @@ export function ascendingId(prefix: 'ses' | 'msg' | 'prt', nowMs: number = Date.
|
|
|
152
154
|
return `${prefix}_${hex}${randomBase62(14)}`
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
// -- Anthropic / OpenAI reasoning-effort mapping (port of bridge.py) ---------
|
|
156
|
-
const ANTHROPIC_THINKING_BUDGETS: Record<string, number> = { high: 16_000, max: 31_999 }
|
|
157
|
-
const ANTHROPIC_ADAPTIVE_THINKING_MODELS = new Set([
|
|
158
|
-
'claude-fable-5',
|
|
159
|
-
'claude-opus-4-6',
|
|
160
|
-
'claude-opus-4-7',
|
|
161
|
-
'claude-opus-4-8',
|
|
162
|
-
'claude-sonnet-4-6',
|
|
163
|
-
])
|
|
164
|
-
const ANTHROPIC_ADAPTIVE_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max'])
|
|
165
|
-
|
|
166
157
|
/**
|
|
167
158
|
* Split a possibly provider-qualified model string into provider + model id,
|
|
168
159
|
* through THE registry parse (`parseModelIdentity`, ORI-2031): the provider is
|
|
@@ -201,34 +192,13 @@ export function buildPromptRequestBody(
|
|
|
201
192
|
// Shared parse (see qualifyHostedModel): keeps the prompt path and the
|
|
202
193
|
// ALI-1086 pre-prompt validation in lockstep by construction.
|
|
203
194
|
const { providerId, modelId } = qualifyHostedModel(model)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
// 'anthropic' default, so its options are unchanged.
|
|
212
|
-
const protocol = findProvider(providerId)?.protocol ?? null
|
|
213
|
-
if (reasoningEffort) {
|
|
214
|
-
if (protocol === 'anthropic-messages') {
|
|
215
|
-
if (ANTHROPIC_ADAPTIVE_THINKING_MODELS.has(modelId)) {
|
|
216
|
-
const options: Record<string, unknown> = { thinking: { type: 'adaptive' } }
|
|
217
|
-
if (ANTHROPIC_ADAPTIVE_EFFORTS.has(reasoningEffort)) {
|
|
218
|
-
options.outputConfig = { effort: reasoningEffort }
|
|
219
|
-
}
|
|
220
|
-
modelSpec.options = options
|
|
221
|
-
} else {
|
|
222
|
-
const budget = ANTHROPIC_THINKING_BUDGETS[reasoningEffort]
|
|
223
|
-
if (budget !== undefined) {
|
|
224
|
-
modelSpec.options = { thinking: { type: 'enabled', budgetTokens: budget } }
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
} else if (protocol === 'openai-responses') {
|
|
228
|
-
modelSpec.options = { reasoningEffort, reasoningSummary: 'auto' }
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
body.model = modelSpec
|
|
195
|
+
body.model = { providerID: providerId, modelID: modelId }
|
|
196
|
+
// Reasoning effort travels as the top-level `variant` (ORI-2526). OpenCode's
|
|
197
|
+
// prompt body accepts only providerID/modelID inside `model` and silently
|
|
198
|
+
// dropped the options we used to send there. Each model's variants map the
|
|
199
|
+
// effort onto its own request fields; an effort the model has no variant for
|
|
200
|
+
// is ignored by OpenCode, so no per-provider mapping lives here.
|
|
201
|
+
if (reasoningEffort) body.variant = reasoningEffort
|
|
232
202
|
return body
|
|
233
203
|
}
|
|
234
204
|
|
|
@@ -571,6 +541,8 @@ export function createOpenCodeHarness(options: OpenCodeHarnessOptions): AgentHar
|
|
|
571
541
|
const userMessageIds = new Set<string>([opencodeMessageId])
|
|
572
542
|
const pendingParts = new Map<string, Array<{ part: Record<string, unknown>; delta: unknown }>>()
|
|
573
543
|
const emittedToolStates = new Set<string>()
|
|
544
|
+
const partTypes = new Map<string, string>()
|
|
545
|
+
const untypedDeltas = new Map<string, string[]>()
|
|
574
546
|
|
|
575
547
|
const handlePart = (part: Record<string, unknown>, delta: unknown): HarnessEvent[] => {
|
|
576
548
|
const partType = typeof part.type === 'string' ? part.type : ''
|
|
@@ -779,16 +751,50 @@ export function createOpenCodeHarness(options: OpenCodeHarnessOptions): AgentHar
|
|
|
779
751
|
}
|
|
780
752
|
}
|
|
781
753
|
}
|
|
782
|
-
} else if (eventType === 'message.part.updated') {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
754
|
+
} else if (eventType === 'message.part.updated' || eventType === 'message.part.delta') {
|
|
755
|
+
// Word-by-word text arrives as `message.part.delta` (ORI-2525), which
|
|
756
|
+
// names the part but not its type. Reasoning parts send deltas too,
|
|
757
|
+
// so a delta streams only once its part is known to be text. A delta
|
|
758
|
+
// can overtake its part's announcement (opencode #26924); it waits
|
|
759
|
+
// here until the announcement says what the part is.
|
|
760
|
+
const updates: Array<{ part: Record<string, unknown>; delta: unknown }> = []
|
|
761
|
+
if (eventType === 'message.part.updated') {
|
|
762
|
+
const part = asRecord(props.part)
|
|
763
|
+
const partId = typeof part.id === 'string' ? part.id : ''
|
|
764
|
+
updates.push({ part, delta: props.delta })
|
|
765
|
+
if (partId && typeof part.type === 'string') partTypes.set(partId, part.type)
|
|
766
|
+
const early = untypedDeltas.get(partId)
|
|
767
|
+
if (early) {
|
|
768
|
+
untypedDeltas.delete(partId)
|
|
769
|
+
const announced = typeof part.text === 'string' ? part.text : ''
|
|
770
|
+
// Replay unless the announced text already includes them.
|
|
771
|
+
if (part.type === 'text' && !announced.startsWith(early.join(''))) {
|
|
772
|
+
for (const delta of early) {
|
|
773
|
+
updates.push({ part: { type: 'text', id: partId, messageID: part.messageID }, delta })
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
const partId = typeof props.partID === 'string' ? props.partID : ''
|
|
779
|
+
const delta = props.delta
|
|
780
|
+
if (props.field !== 'text' || !partId || typeof delta !== 'string') continue
|
|
781
|
+
const partType = partTypes.get(partId)
|
|
782
|
+
if (partType === undefined) {
|
|
783
|
+
untypedDeltas.set(partId, [...(untypedDeltas.get(partId) ?? []), delta])
|
|
784
|
+
continue
|
|
785
|
+
}
|
|
786
|
+
if (partType !== 'text') continue
|
|
787
|
+
updates.push({ part: { type: 'text', id: partId, messageID: props.messageID }, delta })
|
|
788
|
+
}
|
|
789
|
+
for (const { part, delta } of updates) {
|
|
790
|
+
const ocMsgId = typeof part.messageID === 'string' ? part.messageID : ''
|
|
791
|
+
if (allowedAssistantMsgIds.has(ocMsgId)) {
|
|
792
|
+
for (const ev of handlePart(part, delta)) yield ev
|
|
793
|
+
} else if (ocMsgId) {
|
|
794
|
+
const list = pendingParts.get(ocMsgId) ?? []
|
|
795
|
+
list.push({ part, delta })
|
|
796
|
+
pendingParts.set(ocMsgId, list)
|
|
797
|
+
}
|
|
792
798
|
}
|
|
793
799
|
} else if (eventType === 'session.compacted') {
|
|
794
800
|
// Compaction re-chains messages so subsequent assistant parentIDs no
|
|
@@ -910,16 +916,9 @@ function errorEvent(messageId: string, error: unknown): HarnessEvent {
|
|
|
910
916
|
|
|
911
917
|
// -- Pre-prompt model validation (ALI-1086) -----------------------------------
|
|
912
918
|
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
// boot when reachable, cached to <cache>/opencode/models.json, refreshed when
|
|
917
|
-
// stale). When models.dev is unreachable the bundled snapshot is stale
|
|
918
|
-
// (predates claude-opus-4-8) and the FIRST prompt dies inside the SSE stream
|
|
919
|
-
// with an opaque `run_failed: Model not found`. These helpers let the hosted
|
|
920
|
-
// loop ask the RUNNING `opencode serve` — `GET /config/providers` is the
|
|
921
|
-
// runtime's actual resolvable catalog — whether the requested model resolves,
|
|
922
|
-
// BEFORE the prompt is posted, and fail fast naming the alternatives.
|
|
919
|
+
// Images bake a validated catalog. Orizu refreshes it before OpenCode's first
|
|
920
|
+
// read and disables its unchecked background writer (ORI-2513). This validation
|
|
921
|
+
// asks the running server whether the selected model resolves before prompting.
|
|
923
922
|
|
|
924
923
|
/** provider id → model ids the RUNNING opencode instance can resolve. */
|
|
925
924
|
export type OpenCodeModelCatalog = ReadonlyMap<string, readonly string[]>
|
|
@@ -1006,10 +1005,9 @@ export interface AwaitModelResolvableOptions {
|
|
|
1006
1005
|
model: string
|
|
1007
1006
|
fetchImpl?: HarnessFetch
|
|
1008
1007
|
/**
|
|
1009
|
-
*
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1012
|
-
* bridges that window instead of failing a run the refresh would have fixed.
|
|
1008
|
+
* Budget for catalog readiness and transient server reads. Hosted startup
|
|
1009
|
+
* finishes its validated refresh before spawn; this polling remains a
|
|
1010
|
+
* compatibility guard for other callers and startup endpoint delays.
|
|
1013
1011
|
*/
|
|
1014
1012
|
timeoutMs?: number
|
|
1015
1013
|
pollMs?: number
|
|
@@ -1022,7 +1020,7 @@ const MODEL_RESOLVE_POLL_MS = 2_000
|
|
|
1022
1020
|
|
|
1023
1021
|
/**
|
|
1024
1022
|
* Decide whether the requested model is resolvable by the RUNNING opencode,
|
|
1025
|
-
* polling briefly
|
|
1023
|
+
* polling briefly for catalog readiness or transient server failures.
|
|
1026
1024
|
* Fail-open on validator infrastructure, with a DELIBERATE asymmetry on null
|
|
1027
1025
|
* catalog reads:
|
|
1028
1026
|
* - null BEFORE any successful read → `skipped` immediately (dead/absent
|
|
@@ -1207,7 +1205,11 @@ export async function spawnOpenCode(opts: SpawnOpenCodeOptions): Promise<Spawned
|
|
|
1207
1205
|
const env: Record<string, string> = {
|
|
1208
1206
|
OPENCODE_CONFIG_CONTENT: buildOpenCodeConfigContent(opts),
|
|
1209
1207
|
...(opts.env ?? {}),
|
|
1208
|
+
OPENCODE_DISABLE_MODELS_FETCH: '1',
|
|
1210
1209
|
}
|
|
1210
|
+
const model = qualifyHostedModel(opts.model)
|
|
1211
|
+
env.OPENCODE_MODELS_PATH = await refreshHostedModels({ env: { ...process.env, ...env }, required: [`${model.providerId}/${model.modelId}`], fetchImpl: opts.fetchImpl, signal: opts.signal })
|
|
1212
|
+
opts.signal?.throwIfAborted()
|
|
1211
1213
|
const cmd = ['opencode', 'serve', '--port', String(port)]
|
|
1212
1214
|
const spawnImpl =
|
|
1213
1215
|
opts.spawn ??
|
|
@@ -33,16 +33,10 @@ import type { HostedSessionOrigin } from './hosted-runtime-assets.js'
|
|
|
33
33
|
*
|
|
34
34
|
* PIN CONSTRAINT: the hosted OpenCode runtime is HARD-PINNED to
|
|
35
35
|
* `opencode-ai@1.14.41` (`OPENCODE_PINNED_VERSION`, SSE-fragile — do not bump)
|
|
36
|
-
* whose
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* `OPENCODE_DISABLE_MODELS_FETCH`), so with models.dev on the sandbox egress
|
|
41
|
-
* allowlist (#1392, `ORIZU_HOSTED_EGRESS_ALLOWLIST`) this id resolves in new
|
|
42
|
-
* sandboxes. If the catalog fetch is blocked/stale anyway, the loop's
|
|
43
|
-
* pre-prompt validation (`awaitOpenCodeModelResolvable`) fails the run fast,
|
|
44
|
-
* naming the resolvable alternatives. The durable fix for the stale bundled
|
|
45
|
-
* catalog is the ALI-929 harness swap.
|
|
36
|
+
* whose bundled catalog predates this model. Images bake a validated models.dev
|
|
37
|
+
* list into the runtime user's cache. Orizu refreshes that cache before spawning
|
|
38
|
+
* OpenCode, preserving baked models on failure; the native unchecked writer is
|
|
39
|
+
* disabled (ORI-2513). Pre-prompt validation still checks the running server.
|
|
46
40
|
*/
|
|
47
41
|
export const DEFAULT_HOSTED_MODEL = 'anthropic/claude-opus-4-8'
|
|
48
42
|
|
package/src/hosted-loop.ts
CHANGED
|
@@ -231,6 +231,8 @@ export interface RunHostedLoopOptions {
|
|
|
231
231
|
/** Value-free local diagnostics shared with the event sink. Production routes
|
|
232
232
|
* this through hosted-boot's stderr seam; consumer failures are ignored. */
|
|
233
233
|
onDiagnostic?: (message: string) => void
|
|
234
|
+
/** Report genuine harness activity to the session's progress watchdog. */
|
|
235
|
+
onProgress?: () => Promise<void>
|
|
234
236
|
}
|
|
235
237
|
|
|
236
238
|
export interface HostedLoopResult {
|
|
@@ -630,10 +632,9 @@ export async function runHostedLoop(opts: RunHostedLoopOptions): Promise<HostedL
|
|
|
630
632
|
|
|
631
633
|
// Fail-fast model validation (ALI-1086): ask the RUNNING opencode whether
|
|
632
634
|
// the requested model is resolvable BEFORE the first prompt. The pinned
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
// stream with an opaque `run_failed: Model not found`. Now the run fails
|
|
635
|
+
// image catalog is baked and refreshed before spawn (ORI-2513). A model
|
|
636
|
+
// absent from that validated list still needs a clear error rather than
|
|
637
|
+
// dying inside the SSE stream with an opaque Model not found. The run fails
|
|
637
638
|
// immediately with a structured artifact + an error naming the requested
|
|
638
639
|
// id AND the resolvable alternatives. Fail-open on validator
|
|
639
640
|
// infrastructure: an unreachable catalog endpoint (`skipped`) proceeds
|
|
@@ -784,6 +785,7 @@ export async function runHostedLoop(opts: RunHostedLoopOptions): Promise<HostedL
|
|
|
784
785
|
}
|
|
785
786
|
return
|
|
786
787
|
}
|
|
788
|
+
if (event.kind !== 'question_auto_answered') await opts.onProgress?.()
|
|
787
789
|
yield event
|
|
788
790
|
}
|
|
789
791
|
// A signal already durable before prompt startup can make a driver end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdir, rename, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
// The pinned runtime reads this file once: only replace it with a valid list containing the required models.
|
|
6
|
+
export async function refreshHostedModels({ env = process.env, required = [], strict = false, fetchImpl = fetch, signal } = {}) {
|
|
7
|
+
signal?.throwIfAborted()
|
|
8
|
+
const path = env.OPENCODE_MODELS_PATH || join(env.XDG_CACHE_HOME || join(env.HOME || homedir(), '.cache'), 'opencode', 'models.json')
|
|
9
|
+
const temporary = `${path}.${process.pid}.tmp`
|
|
10
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
11
|
+
const strings = (value, keys) => keys.every(key => value[key] === undefined || typeof value[key] === 'string')
|
|
12
|
+
const cost = value => value === undefined || (record(value) && ['input', 'output', 'cache_read', 'cache_write'].every(key => value[key] === undefined || Number.isFinite(value[key])) && cost(value.context_over_200k))
|
|
13
|
+
const providerOptions = value => value === undefined || (record(value) && strings(value, ['api', 'npm']) &&
|
|
14
|
+
(value.body === undefined || record(value.body)) && (value.headers === undefined || (record(value.headers) && Object.values(value.headers).every(header => typeof header === 'string'))))
|
|
15
|
+
const optionalModelFields = model => {
|
|
16
|
+
if (!strings(model, ['family']) || (model.status !== undefined && !['alpha', 'beta', 'deprecated', 'active'].includes(model.status)) || !cost(model.cost) || !providerOptions(model.provider)) return false
|
|
17
|
+
if (model.limit.input !== undefined && !Number.isFinite(model.limit.input)) return false
|
|
18
|
+
if (model.interleaved !== undefined && typeof model.interleaved !== 'boolean' && (!record(model.interleaved) || !['reasoning_content', 'reasoning_details'].includes(model.interleaved.field))) return false
|
|
19
|
+
if (model.modalities !== undefined && (!record(model.modalities) || !['input', 'output'].every(key => Array.isArray(model.modalities[key]) && model.modalities[key].every(value => ['text', 'audio', 'image', 'video', 'pdf'].includes(value))))) return false
|
|
20
|
+
if (model.experimental !== undefined && (!record(model.experimental) || (model.experimental.modes !== undefined && (!record(model.experimental.modes) || Object.entries(model.experimental.modes).some(([name, mode]) => !name || !record(mode) || !cost(mode.cost) || !providerOptions(mode.provider)))))) return false
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
const identities = text => {
|
|
24
|
+
const catalog = JSON.parse(text)
|
|
25
|
+
if (!catalog || typeof catalog !== 'object' || Array.isArray(catalog)) throw new Error('Invalid model catalog')
|
|
26
|
+
return Object.entries(catalog).flatMap(([provider, value]) => {
|
|
27
|
+
if (!value || value.id !== provider || !strings(value, ['api', 'npm']) || typeof value.name !== 'string' || !Array.isArray(value.env) || value.env.some(name => typeof name !== 'string') || typeof value.models !== 'object' || !value.models || Array.isArray(value.models)) throw new Error('Invalid model provider')
|
|
28
|
+
return Object.entries(value.models).map(([id, model]) => {
|
|
29
|
+
if (!model || model.id !== id || typeof model.name !== 'string' || typeof model.release_date !== 'string' || ['attachment', 'reasoning', 'tool_call'].some(key => typeof model[key] !== 'boolean') || (model.temperature !== undefined && typeof model.temperature !== 'boolean') || !Number.isFinite(model.limit?.context) || !Number.isFinite(model.limit?.output)) throw new Error('Invalid model entry')
|
|
30
|
+
if (!optionalModelFields(model)) throw new Error('Invalid optional model metadata')
|
|
31
|
+
return ['alpha', 'deprecated'].includes(model.status) ? null : `${provider}/${id}`
|
|
32
|
+
}).filter(Boolean)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetchImpl(`${env.OPENCODE_MODELS_URL || 'https://models.dev'}/api.json`, { signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(10_000)]) : AbortSignal.timeout(10_000) })
|
|
37
|
+
if (!response.ok) throw new Error(`Model catalog download failed: ${response.status}`)
|
|
38
|
+
const text = await response.text()
|
|
39
|
+
const models = new Set(identities(text))
|
|
40
|
+
if (!models.size || required.some(id => !models.has(id))) throw new Error('Model catalog is missing required models')
|
|
41
|
+
await mkdir(dirname(path), { recursive: true })
|
|
42
|
+
await writeFile(temporary, text)
|
|
43
|
+
signal?.throwIfAborted()
|
|
44
|
+
await rename(temporary, path)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
signal?.throwIfAborted()
|
|
47
|
+
if (strict) throw error
|
|
48
|
+
} finally { await rm(temporary, { force: true }) }
|
|
49
|
+
signal?.throwIfAborted()
|
|
50
|
+
return path
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const bakeHostedModels = () => refreshHostedModels({ strict: true, required: process.env.HOSTED_WORKSPACE_DEFAULT_MODEL ? [process.env.HOSTED_WORKSPACE_DEFAULT_MODEL] : ['openai/gpt-5.6', 'openai/gpt-6-luna', 'anthropic/claude-opus-4-8'] })
|
package/src/index.ts
CHANGED
|
@@ -155,6 +155,8 @@ import {
|
|
|
155
155
|
setDatasetReadme,
|
|
156
156
|
createDatasetVersion,
|
|
157
157
|
createDatasetSplitSet,
|
|
158
|
+
listDatasetSplitSets,
|
|
159
|
+
showDatasetSplitSet,
|
|
158
160
|
editDatasetRows,
|
|
159
161
|
deleteDatasetRows,
|
|
160
162
|
deleteDataset,
|
|
@@ -2139,6 +2141,14 @@ export async function main(rawArgs = process.argv.slice(2)) {
|
|
|
2139
2141
|
await createDatasetVersion()
|
|
2140
2142
|
return
|
|
2141
2143
|
}
|
|
2144
|
+
if (command === 'datasets' && subcommand === 'splits' && datasetsAction === 'list') {
|
|
2145
|
+
await listDatasetSplitSets()
|
|
2146
|
+
return
|
|
2147
|
+
}
|
|
2148
|
+
if (command === 'datasets' && subcommand === 'splits' && datasetsAction === 'show') {
|
|
2149
|
+
await showDatasetSplitSet()
|
|
2150
|
+
return
|
|
2151
|
+
}
|
|
2142
2152
|
if (command === 'datasets' && subcommand === 'splits' && datasetsAction === 'create') {
|
|
2143
2153
|
await createDatasetSplitSet()
|
|
2144
2154
|
return
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Cause, Effect, Exit } from 'effect'
|
|
2
|
+
|
|
3
|
+
import { authedFetch } from './http.js'
|
|
4
|
+
import { responsePayload } from './json-response.js'
|
|
5
|
+
import { stageSyncPayload, startSyncLockClaim, tryClaimSyncLock, type SyncDiskOptions, type SyncDiskResult, type SyncPayload, type SyncRequestPlan } from './instruction-set-sync/index.js'
|
|
6
|
+
import { applyUpdate, lockedProfileIdentity, makeUpdatePlan, readUpdateLock, type UpdatePlan, type UpdateResult } from './instruction-set-update/index.js'
|
|
7
|
+
|
|
8
|
+
// Loaded only through `await import(...)` from instruction-sets-cli, so CLI
|
|
9
|
+
// startup and the published loader never load Effect.
|
|
10
|
+
|
|
11
|
+
export interface Interrupted { interrupted: 130 | 143 }
|
|
12
|
+
|
|
13
|
+
// Existing file steps throw plain errors; keep them as the failure value so the
|
|
14
|
+
// CLI prints exactly the same message.
|
|
15
|
+
const attempt = <A>(operation: () => A) => Effect.try({ try: operation, catch: cause => cause })
|
|
16
|
+
|
|
17
|
+
function readPayload(path: string, action: string) {
|
|
18
|
+
return Effect.tryPromise({
|
|
19
|
+
// The body stays unchecked here, as before: stageSyncPayload and makeUpdatePlan validate it.
|
|
20
|
+
// SAFETY: their existing error messages for malformed bodies are part of the CLI's output.
|
|
21
|
+
try: async signal => await responsePayload(await authedFetch(path, { method: 'GET', signal }), action) as unknown as SyncPayload,
|
|
22
|
+
catch: cause => cause,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Waiting for another process's lock is the only interruptible part: nothing
|
|
27
|
+
// is held yet. Publishing runs to completion once the lock is ours.
|
|
28
|
+
function holdSyncLock<A>(appRoot: string, use: Effect.Effect<A, unknown>) {
|
|
29
|
+
const acquire = Effect.gen(function*() {
|
|
30
|
+
const claim = yield* attempt(() => startSyncLockClaim(appRoot))
|
|
31
|
+
while (true) {
|
|
32
|
+
const claimed = yield* attempt(() => tryClaimSyncLock(claim))
|
|
33
|
+
if (typeof claimed === 'function') return claimed
|
|
34
|
+
yield* Effect.interruptible(Effect.sleep(claimed))
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
return Effect.acquireUseRelease(acquire, () => use, release => Effect.sync(release))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Stage the version, publish it under the lock, and always remove an
|
|
41
|
+
// unpublished stage, including after Ctrl-C or SIGTERM.
|
|
42
|
+
function deliverPayload(...args: Parameters<typeof stageSyncPayload>): Effect.Effect<SyncDiskResult, unknown> {
|
|
43
|
+
return Effect.acquireUseRelease(
|
|
44
|
+
attempt(() => stageSyncPayload(...args)),
|
|
45
|
+
staged => holdSyncLock(staged.appRoot, attempt(staged.publish)).pipe(
|
|
46
|
+
Effect.map(warnings => ({ destination: staged.destination, wasPresent: staged.wasPresent, warnings })),
|
|
47
|
+
),
|
|
48
|
+
staged => Effect.sync(staged.discard),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function runOrThrow<A>(program: Effect.Effect<A, unknown>, signal: AbortSignal): Promise<A> {
|
|
53
|
+
const exit = await Effect.runPromiseExit(program, { signal })
|
|
54
|
+
if (Exit.isSuccess(exit)) return exit.value
|
|
55
|
+
throw Cause.squash(exit.cause)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// `once`: a second signal falls through to Node's default kill.
|
|
59
|
+
async function runDelivery<A>(program: (signal: AbortSignal) => Effect.Effect<A, unknown>): Promise<A | Interrupted> {
|
|
60
|
+
const controller = new AbortController()
|
|
61
|
+
let interrupted: Interrupted['interrupted'] | undefined
|
|
62
|
+
const handleInterrupt = () => { interrupted ??= 130; controller.abort() }
|
|
63
|
+
const handleTerminate = () => { interrupted ??= 143; controller.abort() }
|
|
64
|
+
process.once('SIGINT', handleInterrupt)
|
|
65
|
+
process.once('SIGTERM', handleTerminate)
|
|
66
|
+
try {
|
|
67
|
+
return await runOrThrow(program(controller.signal), controller.signal)
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (interrupted !== undefined) return { interrupted }
|
|
70
|
+
throw error
|
|
71
|
+
} finally {
|
|
72
|
+
process.removeListener('SIGINT', handleInterrupt)
|
|
73
|
+
process.removeListener('SIGTERM', handleTerminate)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function syncInstructionSet(out: string, project: string, plan: SyncRequestPlan, options: SyncDiskOptions) {
|
|
78
|
+
return runDelivery(() => Effect.gen(function*() {
|
|
79
|
+
const payload = yield* readPayload(plan.path, 'Instruction sets sync')
|
|
80
|
+
const result = yield* deliverPayload(out, project, plan, payload, options)
|
|
81
|
+
return { payload, result }
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function updateInstructionSets(
|
|
86
|
+
out: string,
|
|
87
|
+
project: string,
|
|
88
|
+
isApproved: boolean,
|
|
89
|
+
noSync: boolean,
|
|
90
|
+
): Promise<{ plan: UpdatePlan; result: UpdateResult } | Interrupted> {
|
|
91
|
+
const program = (signal: AbortSignal) => Effect.gen(function*() {
|
|
92
|
+
const lock = yield* attempt(() => readUpdateLock(out, project))
|
|
93
|
+
const payloads: SyncPayload[] = []
|
|
94
|
+
for (const [setSlug, set] of Object.entries(lock.instructionSets)) {
|
|
95
|
+
const barePath = `/api/cli/instruction-sets/${encodeURIComponent(setSlug)}/sync?project=${encodeURIComponent(project)}`
|
|
96
|
+
payloads.push(yield* readPayload(barePath, 'Instruction sets update'))
|
|
97
|
+
for (const profileSlugValue of Object.keys(set.profiles)) {
|
|
98
|
+
const identity = yield* attempt(() => lockedProfileIdentity(out, setSlug, profileSlugValue))
|
|
99
|
+
const query = new URLSearchParams({ project, profile: identity })
|
|
100
|
+
const path = `/api/cli/instruction-sets/${encodeURIComponent(setSlug)}/sync?${query.toString()}`
|
|
101
|
+
payloads.push(yield* readPayload(path, 'Instruction sets update'))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const plan = yield* attempt(() => makeUpdatePlan(project, lock, payloads))
|
|
105
|
+
if (!isApproved) return { plan, result: { absent: [], warnings: [] } }
|
|
106
|
+
// applyUpdate's own steps are not interruptible; each version it syncs is
|
|
107
|
+
// a nested delivery that the shared signal interrupts and cleans up.
|
|
108
|
+
const result = yield* Effect.uninterruptible(Effect.tryPromise({
|
|
109
|
+
try: () => applyUpdate(out, project, plan, noSync, undefined,
|
|
110
|
+
(...args) => runOrThrow(deliverPayload(...args), signal)),
|
|
111
|
+
catch: cause => cause,
|
|
112
|
+
}))
|
|
113
|
+
return { plan, result }
|
|
114
|
+
})
|
|
115
|
+
// --no-sync stages nothing to clean up, so keep Node's default signal
|
|
116
|
+
// handling: a signal stops it at once instead of being held until the
|
|
117
|
+
// update has already landed.
|
|
118
|
+
if (noSync) {
|
|
119
|
+
const signal = new AbortController().signal
|
|
120
|
+
return runOrThrow(program(signal), signal)
|
|
121
|
+
}
|
|
122
|
+
return runDelivery(program)
|
|
123
|
+
}
|
|
@@ -509,17 +509,25 @@ function removeObservedSyncLock(lockPath: string, observed: ObservedSyncLock): b
|
|
|
509
509
|
return true
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
-
export
|
|
512
|
+
export interface SyncLockClaim { lockPath: string; metadata: string; deadline: number }
|
|
513
|
+
|
|
514
|
+
export function startSyncLockClaim(appRoot: string): SyncLockClaim {
|
|
513
515
|
mkdirSync(appRoot, { recursive: true })
|
|
514
|
-
const lockPath = join(appRoot, '.orizu.lock.json.lock')
|
|
515
|
-
const metadata = `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`
|
|
516
516
|
const lockWaitMs = processTestMilliseconds(
|
|
517
517
|
'ORIZU_TEST_SYNC_LOCK_WAIT_MS',
|
|
518
518
|
100,
|
|
519
519
|
SYNC_LOCK_WAIT_MS
|
|
520
520
|
) ?? SYNC_LOCK_WAIT_MS
|
|
521
|
-
|
|
521
|
+
return {
|
|
522
|
+
lockPath: join(appRoot, '.orizu.lock.json.lock'),
|
|
523
|
+
metadata: `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`,
|
|
524
|
+
deadline: Date.now() + lockWaitMs,
|
|
525
|
+
}
|
|
526
|
+
}
|
|
522
527
|
|
|
528
|
+
// One claim attempt: returns the release function, or how long to wait before
|
|
529
|
+
// trying again. Waiting is the caller's job so it can be blocking or not.
|
|
530
|
+
export function tryClaimSyncLock({ lockPath, metadata, deadline }: SyncLockClaim): (() => void) | number {
|
|
523
531
|
while (true) {
|
|
524
532
|
try {
|
|
525
533
|
writeFileSync(lockPath, metadata, { encoding: 'utf8', flag: 'wx', mode: 0o600 })
|
|
@@ -540,17 +548,26 @@ export function withSyncLock<T>(appRoot: string, operation: () => T): T {
|
|
|
540
548
|
if (Date.now() >= deadline) {
|
|
541
549
|
throw new Error(`instruction_set_sync_lock_busy${busyPid === undefined ? '' : `:${busyPid}`}`)
|
|
542
550
|
}
|
|
543
|
-
|
|
551
|
+
return Math.min(SYNC_LOCK_RETRY_MS, Math.max(1, deadline - Date.now()))
|
|
544
552
|
}
|
|
545
553
|
}
|
|
546
554
|
|
|
547
|
-
|
|
555
|
+
return () => {
|
|
548
556
|
try {
|
|
549
557
|
if (readFileSync(lockPath, 'utf8') === metadata) rmSync(lockPath)
|
|
550
558
|
} catch (error) {
|
|
551
559
|
if (!(isNodeError(error) && error.code === 'ENOENT')) throw error
|
|
552
560
|
}
|
|
553
561
|
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function withSyncLock<T>(appRoot: string, operation: () => T): T {
|
|
565
|
+
const claim = startSyncLockClaim(appRoot)
|
|
566
|
+
let release = tryClaimSyncLock(claim)
|
|
567
|
+
while (typeof release === 'number') {
|
|
568
|
+
sleepSync(release)
|
|
569
|
+
release = tryClaimSyncLock(claim)
|
|
570
|
+
}
|
|
554
571
|
let result: T
|
|
555
572
|
try {
|
|
556
573
|
result = operation()
|
|
@@ -600,6 +617,16 @@ export interface SyncDiskOptions {
|
|
|
600
617
|
platform?: NodeJS.Platform
|
|
601
618
|
}
|
|
602
619
|
|
|
620
|
+
export interface StagedSync {
|
|
621
|
+
appRoot: string
|
|
622
|
+
destination: string
|
|
623
|
+
wasPresent: boolean
|
|
624
|
+
/** Publishes the staged version; the caller must hold the folder lock. Returns warnings. */
|
|
625
|
+
publish: () => string[]
|
|
626
|
+
/** Removes the staged version if it was not published. Safe to call more than once. */
|
|
627
|
+
discard: () => void
|
|
628
|
+
}
|
|
629
|
+
|
|
603
630
|
export function syncPayloadToDisk(
|
|
604
631
|
out: string,
|
|
605
632
|
project: string,
|
|
@@ -607,6 +634,22 @@ export function syncPayloadToDisk(
|
|
|
607
634
|
payload: SyncPayload,
|
|
608
635
|
options: SyncDiskOptions = {}
|
|
609
636
|
): SyncDiskResult {
|
|
637
|
+
const staged = stageSyncPayload(out, project, plan, payload, options)
|
|
638
|
+
try {
|
|
639
|
+
const warnings = withSyncLock(staged.appRoot, staged.publish)
|
|
640
|
+
return { destination: staged.destination, wasPresent: staged.wasPresent, warnings }
|
|
641
|
+
} finally {
|
|
642
|
+
staged.discard()
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function stageSyncPayload(
|
|
647
|
+
out: string,
|
|
648
|
+
project: string,
|
|
649
|
+
plan: SyncRequestPlan,
|
|
650
|
+
payload: SyncPayload,
|
|
651
|
+
options: SyncDiskOptions = {}
|
|
652
|
+
): StagedSync {
|
|
610
653
|
const target = parseSyncTarget(options.target)
|
|
611
654
|
const now = options.now ?? (() => new Date())
|
|
612
655
|
const set = payload.instructionSet
|
|
@@ -717,9 +760,11 @@ export function syncPayloadToDisk(
|
|
|
717
760
|
assertExistingVersionMatches(destination, componentHashes, manifestBytes, generatedBytes)
|
|
718
761
|
}
|
|
719
762
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
763
|
+
return {
|
|
764
|
+
appRoot,
|
|
765
|
+
destination,
|
|
766
|
+
wasPresent,
|
|
767
|
+
publish: () => {
|
|
723
768
|
holdForProcessTest('ORIZU_TEST_SYNC_HOLD_BEFORE_PUBLISH_MS')
|
|
724
769
|
const currentLock = readLock(appRoot, project)
|
|
725
770
|
holdForProcessTest('ORIZU_TEST_SYNC_HOLD_LOCK_MS')
|
|
@@ -786,8 +831,9 @@ export function syncPayloadToDisk(
|
|
|
786
831
|
}
|
|
787
832
|
stagedTemp = null
|
|
788
833
|
}
|
|
789
|
-
warnings = emitManagedArtifacts(out, appRoot, currentLock, options.forceHelpers ?? false, target, journal)
|
|
834
|
+
const warnings = emitManagedArtifacts(out, appRoot, currentLock, options.forceHelpers ?? false, target, journal)
|
|
790
835
|
journal.write(join(appRoot, 'orizu.lock.json'), serializeLock(currentLock))
|
|
836
|
+
return warnings
|
|
791
837
|
} catch (error) {
|
|
792
838
|
journal.rollback()
|
|
793
839
|
currentLock.helpers = priorHelpers
|
|
@@ -802,9 +848,10 @@ export function syncPayloadToDisk(
|
|
|
802
848
|
}
|
|
803
849
|
throw error
|
|
804
850
|
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
|
|
851
|
+
},
|
|
852
|
+
discard: () => {
|
|
853
|
+
if (stagedTemp) rmSync(stagedTemp, { recursive: true, force: true })
|
|
854
|
+
stagedTemp = null
|
|
855
|
+
},
|
|
808
856
|
}
|
|
809
|
-
return { destination, wasPresent, warnings }
|
|
810
857
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type InstructionSetLockV1,
|
|
12
12
|
} from '../instruction-set-lock/index.js'
|
|
13
13
|
import { assertOutputConfined, reconcileGeneratedIndex } from '../instruction-set-sync/helpers.js'
|
|
14
|
-
import { profileSlug, recordResolvedPayloadInLock, syncPayloadToDisk, withSyncLock, type SyncPayload } from '../instruction-set-sync/index.js'
|
|
14
|
+
import { profileSlug, recordResolvedPayloadInLock, syncPayloadToDisk, withSyncLock, type SyncDiskResult, type SyncPayload } from '../instruction-set-sync/index.js'
|
|
15
15
|
import { verifyInstructionSetTree, verifyMaterializedVersionAgainstLock } from '../instruction-set-verify/index.js'
|
|
16
16
|
|
|
17
17
|
export interface UpdateResolution {
|
|
@@ -222,7 +222,8 @@ export async function applyUpdate(
|
|
|
222
222
|
project: string,
|
|
223
223
|
plan: UpdatePlan,
|
|
224
224
|
noSync: boolean,
|
|
225
|
-
now: () => Date = () => new Date()
|
|
225
|
+
now: () => Date = () => new Date(),
|
|
226
|
+
sync: (...args: Parameters<typeof syncPayloadToDisk>) => SyncDiskResult | Promise<SyncDiskResult> = syncPayloadToDisk
|
|
226
227
|
): Promise<UpdateResult> {
|
|
227
228
|
const absent: string[] = []
|
|
228
229
|
const warnings: string[] = []
|
|
@@ -271,7 +272,7 @@ export async function applyUpdate(
|
|
|
271
272
|
let lock = plan.lock
|
|
272
273
|
for (const resolution of plan.resolutions) {
|
|
273
274
|
const set = resolution.payload.instructionSet
|
|
274
|
-
const result =
|
|
275
|
+
const result = await sync(out, project, {
|
|
275
276
|
parsed: { set: set.slug, profile: resolution.profileIdentity },
|
|
276
277
|
path: '',
|
|
277
278
|
lock,
|