@oxygen-agent/cli 1.1014.19 → 1.1014.81
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 +1 -1
- package/dist/command-manifest.js +1 -1
- package/dist/help.d.ts +1 -1
- package/dist/help.js +25 -3
- package/dist/index.js +81 -16
- package/dist/json-output.d.ts +3 -0
- package/dist/json-output.js +33 -0
- package/dist/workspace-skill-commands.d.ts +9 -0
- package/dist/workspace-skill-commands.js +135 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +9 -0
- package/node_modules/@oxygen/shared/dist/column-decision.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
- package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.js +7 -4
- package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +18 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-skills.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-skills.js +62 -0
- package/node_modules/@oxygen/shared/dist/dev-eval-classifier.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/dev-eval-classifier.js +4 -0
- package/node_modules/@oxygen/shared/dist/dev-eval-context.d.ts +35 -0
- package/node_modules/@oxygen/shared/dist/dev-eval-context.js +108 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.js +5 -1
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +178 -63
- package/node_modules/@oxygen/shared/dist/email-health-status.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/email-health-status.js +16 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +3 -2
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +9 -6
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +4 -2
- package/node_modules/@oxygen/shared/dist/langfuse.js +123 -66
- package/node_modules/@oxygen/shared/dist/table-run-summary.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/table-run-summary.js +26 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-skills.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/workspace-skills.js +102 -0
- package/package.json +1 -1
|
@@ -184,8 +184,9 @@ export declare function resolveHostedAiModel(input: {
|
|
|
184
184
|
export declare function managedModelLabel(model: string, preferredUseCase?: HostedAiUseCase): string | null;
|
|
185
185
|
/**
|
|
186
186
|
* What an AI-column run's provenance records as its model: the tier label for
|
|
187
|
-
* a managed run,
|
|
188
|
-
* their own key. A BYOK run on the tier
|
|
187
|
+
* a managed generative run, System One for classification, and the model itself
|
|
188
|
+
* only when the customer pinned it on their own key. A BYOK run on the tier
|
|
189
|
+
* default is still OXYGEN's pick.
|
|
189
190
|
*/
|
|
190
191
|
export declare function customerFacingAiColumnModel(input: {
|
|
191
192
|
model: string;
|
|
@@ -481,11 +481,11 @@ export function resolveHostedAiModel(input) {
|
|
|
481
481
|
const BACKUP_MODEL_LABEL = "Oxygen backup model";
|
|
482
482
|
// TypeSafe's System One classifier (`SYSTEM_ONE_MODEL` in @oxygen/integrations,
|
|
483
483
|
// pinned as `jev-<version>`) is OXYGEN's choice too. It answers classification
|
|
484
|
-
// columns and inbox triage
|
|
485
|
-
//
|
|
486
|
-
//
|
|
484
|
+
// columns and inbox triage without text generation. Its flat classification
|
|
485
|
+
// price and inference class differ from the generative tiers. Keep a distinct
|
|
486
|
+
// label while hiding the managed vendor id, including after a version bump.
|
|
487
487
|
const SYSTEM_ONE_MODEL_ID = /^jev-(?:\d+(?:\.\d+)*|latest)$/;
|
|
488
|
-
const SYSTEM_ONE_MODEL_LABEL =
|
|
488
|
+
const SYSTEM_ONE_MODEL_LABEL = "System One";
|
|
489
489
|
// Doppler can swap any tier's model without a deploy; the swapped-in id is still
|
|
490
490
|
// OXYGEN's choice, so it is redacted like the registry default it replaces.
|
|
491
491
|
const MODEL_OVERRIDE_ENV_PREFIXES = ["OXYGEN_OPENROUTER_MODEL_", "OXYGEN_COPILOT_MODEL_", "OXYGEN_AGENT_MODEL_"];
|
|
@@ -566,10 +566,13 @@ function labelForManagedModel(labels, model) {
|
|
|
566
566
|
}
|
|
567
567
|
/**
|
|
568
568
|
* What an AI-column run's provenance records as its model: the tier label for
|
|
569
|
-
* a managed run,
|
|
570
|
-
* their own key. A BYOK run on the tier
|
|
569
|
+
* a managed generative run, System One for classification, and the model itself
|
|
570
|
+
* only when the customer pinned it on their own key. A BYOK run on the tier
|
|
571
|
+
* default is still OXYGEN's pick.
|
|
571
572
|
*/
|
|
572
573
|
export function customerFacingAiColumnModel(input) {
|
|
574
|
+
if (SYSTEM_ONE_MODEL_ID.test(input.model))
|
|
575
|
+
return SYSTEM_ONE_MODEL_LABEL;
|
|
573
576
|
if (input.credentialMode === "byok" && managedModelLabel(input.model, "ai_column") === null)
|
|
574
577
|
return input.model;
|
|
575
578
|
return MANAGED_AI_TIER_LABELS[input.reasoningLevel];
|
|
@@ -105,7 +105,7 @@ export type LlmTracingClient = {
|
|
|
105
105
|
score(body: LlmScoreBody): Promise<boolean>;
|
|
106
106
|
/** Never rejects; bounded at 5s on workers, 15s in serverless after(). */
|
|
107
107
|
flush(): Promise<void>;
|
|
108
|
-
/** Flush + stop timers.
|
|
108
|
+
/** Flush + stop timers. Never rejects; bounded at 30s to drain active retries. */
|
|
109
109
|
shutdown(): Promise<void>;
|
|
110
110
|
};
|
|
111
111
|
/**
|
|
@@ -178,12 +178,14 @@ export type LlmExportSummary = {
|
|
|
178
178
|
* spans, so exported:dropped is not a like-for-like ratio.
|
|
179
179
|
*/
|
|
180
180
|
dropped(count: number): void;
|
|
181
|
+
retried(): void;
|
|
182
|
+
recovered(count: number): void;
|
|
181
183
|
/** Logs the window when it is at least a minute old; otherwise no-op. */
|
|
182
184
|
maybeReport(): void;
|
|
183
185
|
/** Logs whatever is pending regardless of age (process shutdown). */
|
|
184
186
|
reportNow(): void;
|
|
185
187
|
};
|
|
186
|
-
export declare function createLlmExportSummary(now?: () => number): LlmExportSummary;
|
|
188
|
+
export declare function createLlmExportSummary(now?: () => number, context?: Record<string, unknown>): LlmExportSummary;
|
|
187
189
|
/**
|
|
188
190
|
* Resolves a registered code prompt (name + content hash) to the native
|
|
189
191
|
* Langfuse prompt version the deploy-time sync created for it. Synchronous
|
|
@@ -47,6 +47,10 @@ const WARN_THROTTLE_MS = 30_000;
|
|
|
47
47
|
* meant every flush that ran long lost its spans instead of finishing late.
|
|
48
48
|
*/
|
|
49
49
|
const LANGFUSE_EXPORT_TIMEOUT_SECONDS = 10;
|
|
50
|
+
// Keep retries below BSP's 30s batch deadline. Shutdown must outlive delivery;
|
|
51
|
+
// ordinary flush callers still return at their existing fail-open bound.
|
|
52
|
+
const LANGFUSE_EXPORT_BUDGET_MS = 25_000;
|
|
53
|
+
const LANGFUSE_SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
50
54
|
// Scores are API strings; unlike observation payloads they cannot use chunks.
|
|
51
55
|
const MAX_JSON_FIELD_CHARS = 400_000;
|
|
52
56
|
/**
|
|
@@ -183,10 +187,12 @@ function compact(body) {
|
|
|
183
187
|
// many spans Langfuse acknowledged, how many batches it refused, and how many
|
|
184
188
|
// observations never reached the exporter at all. Counts only; never content.
|
|
185
189
|
const EXPORT_SUMMARY_INTERVAL_MS = 60_000;
|
|
186
|
-
export function createLlmExportSummary(now = Date.now) {
|
|
190
|
+
export function createLlmExportSummary(now = Date.now, context = {}) {
|
|
187
191
|
let exported = 0;
|
|
188
192
|
let failed = 0;
|
|
189
193
|
let dropped = 0;
|
|
194
|
+
let retried = 0;
|
|
195
|
+
let recovered = 0;
|
|
190
196
|
let windowStartedAt = now();
|
|
191
197
|
const report = () => {
|
|
192
198
|
const at = now();
|
|
@@ -196,12 +202,17 @@ export function createLlmExportSummary(now = Date.now) {
|
|
|
196
202
|
exported,
|
|
197
203
|
failed,
|
|
198
204
|
dropped,
|
|
205
|
+
retried,
|
|
206
|
+
recovered,
|
|
207
|
+
...context,
|
|
199
208
|
window_seconds: Math.max(0, Math.round((at - windowStartedAt) / 1000)),
|
|
200
209
|
});
|
|
201
210
|
}
|
|
202
211
|
exported = 0;
|
|
203
212
|
failed = 0;
|
|
204
213
|
dropped = 0;
|
|
214
|
+
retried = 0;
|
|
215
|
+
recovered = 0;
|
|
205
216
|
windowStartedAt = at;
|
|
206
217
|
};
|
|
207
218
|
const add = (count) => (Number.isFinite(count) && count > 0 ? Math.floor(count) : 0);
|
|
@@ -209,6 +220,8 @@ export function createLlmExportSummary(now = Date.now) {
|
|
|
209
220
|
exported: (count) => { exported += add(count); },
|
|
210
221
|
failed: (count) => { failed += add(count); },
|
|
211
222
|
dropped: (count) => { dropped += add(count); },
|
|
223
|
+
retried: () => { retried++; },
|
|
224
|
+
recovered: (count) => { recovered += add(count); },
|
|
212
225
|
maybeReport: () => { if (now() - windowStartedAt >= EXPORT_SUMMARY_INTERVAL_MS)
|
|
213
226
|
report(); },
|
|
214
227
|
reportNow: report,
|
|
@@ -270,75 +283,95 @@ function createOtelEmitter(env, warn, summary) {
|
|
|
270
283
|
if (!encoded)
|
|
271
284
|
throw new Error("serialization_failed");
|
|
272
285
|
const body = new TextDecoder().decode(encoded);
|
|
273
|
-
const
|
|
274
|
-
let
|
|
286
|
+
const startedAt = Date.now();
|
|
287
|
+
let status;
|
|
288
|
+
let failureKind = "transport_failed";
|
|
289
|
+
let attempts = 0;
|
|
275
290
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
if (signal.aborted)
|
|
281
|
-
throw new Error("deadline_exceeded");
|
|
282
|
-
if (attempt === 2)
|
|
283
|
-
throw new Error("network_failed");
|
|
284
|
-
await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
if (![429, 502, 503, 504].includes(response.status) || attempt === 2)
|
|
291
|
+
const remaining = LANGFUSE_EXPORT_BUDGET_MS - (Date.now() - startedAt);
|
|
292
|
+
if (remaining <= 0)
|
|
288
293
|
break;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (!response?.ok) {
|
|
294
|
-
await response?.body?.cancel().catch(() => { });
|
|
295
|
-
throw new Error("http_rejected");
|
|
296
|
-
}
|
|
297
|
-
const chunks = [];
|
|
298
|
-
let bytes = 0;
|
|
299
|
-
const reader = response.body?.getReader();
|
|
300
|
-
if (reader) {
|
|
294
|
+
attempts = attempt + 1;
|
|
295
|
+
status = undefined;
|
|
296
|
+
const signal = AbortSignal.timeout(Math.min(LANGFUSE_EXPORT_TIMEOUT_SECONDS * 1000, remaining));
|
|
297
|
+
let response;
|
|
301
298
|
try {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// OTLP acknowledgements are tiny; an echoed payload must never
|
|
308
|
-
// become an unbounded allocation or reach any diagnostic logger.
|
|
309
|
-
if (bytes > 64 * 1024)
|
|
310
|
-
throw new Error("response_too_large");
|
|
311
|
-
chunks.push(next.value);
|
|
299
|
+
try {
|
|
300
|
+
response = await fetch(exportUrl, { method: "POST", headers: exportHeaders, body, signal, redirect: "error" });
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
throw new Error(signal.aborted ? "deadline_exceeded" : "network_failed");
|
|
312
304
|
}
|
|
305
|
+
status = response.status;
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
await response.body?.cancel().catch(() => { });
|
|
308
|
+
throw new Error("http_rejected");
|
|
309
|
+
}
|
|
310
|
+
const chunks = [];
|
|
311
|
+
let bytes = 0;
|
|
312
|
+
const reader = response.body?.getReader();
|
|
313
|
+
if (reader) {
|
|
314
|
+
try {
|
|
315
|
+
for (;;) {
|
|
316
|
+
const next = await reader.read();
|
|
317
|
+
if (next.done)
|
|
318
|
+
break;
|
|
319
|
+
bytes += next.value.byteLength;
|
|
320
|
+
// Never allocate or log an unbounded echoed private payload.
|
|
321
|
+
if (bytes > 64 * 1024)
|
|
322
|
+
throw new Error("response_too_large");
|
|
323
|
+
chunks.push(next.value);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
await reader.cancel().catch(() => { });
|
|
328
|
+
throw new Error(signal.aborted ? "deadline_exceeded" : error instanceof Error && error.message === "response_too_large" ? "response_too_large" : "response_read_failed");
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
reader.releaseLock();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
let acknowledgement;
|
|
335
|
+
try {
|
|
336
|
+
acknowledgement = bytes ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : {};
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
throw new Error("response_invalid");
|
|
340
|
+
}
|
|
341
|
+
if (!acknowledgement || typeof acknowledgement !== "object" || Array.isArray(acknowledgement))
|
|
342
|
+
throw new Error("response_invalid");
|
|
343
|
+
const envelope = acknowledgement;
|
|
344
|
+
const partial = envelope.partialSuccess ?? envelope.partial_success;
|
|
345
|
+
if (partial !== undefined && partial !== null) {
|
|
346
|
+
if (typeof partial !== "object" || Array.isArray(partial))
|
|
347
|
+
throw new Error("response_invalid");
|
|
348
|
+
const rawRejected = partial.rejectedSpans ?? partial.rejected_spans ?? 0;
|
|
349
|
+
if ((typeof rawRejected !== "number" && typeof rawRejected !== "string") || !/^\d+$/.test(String(rawRejected)))
|
|
350
|
+
throw new Error("response_invalid");
|
|
351
|
+
if (Number(rawRejected) > 0)
|
|
352
|
+
throw new Error("partial_rejection");
|
|
353
|
+
}
|
|
354
|
+
if (attempt > 0)
|
|
355
|
+
summary.recovered(spans.length);
|
|
356
|
+
return;
|
|
313
357
|
}
|
|
314
358
|
catch (error) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
359
|
+
failureKind = error instanceof Error ? error.message : "transport_failed";
|
|
360
|
+
const retryable = ["deadline_exceeded", "network_failed", "response_read_failed"].includes(failureKind)
|
|
361
|
+
|| (failureKind === "http_rejected" && [429, 502, 503, 504].includes(status ?? 0));
|
|
362
|
+
if (!retryable || attempt === 2) {
|
|
363
|
+
// Carry fixed classifications and numbers only, never an SDK or
|
|
364
|
+
// receiver error that could quote the request/response payload.
|
|
365
|
+
throw Object.assign(new Error(failureKind), { attempts: attempt + 1, status });
|
|
366
|
+
}
|
|
367
|
+
const pause = 100 * (attempt + 1);
|
|
368
|
+
if (LANGFUSE_EXPORT_BUDGET_MS - (Date.now() - startedAt) <= pause)
|
|
369
|
+
break;
|
|
370
|
+
summary.retried();
|
|
371
|
+
await new Promise((resolve) => setTimeout(resolve, pause));
|
|
320
372
|
}
|
|
321
373
|
}
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
acknowledgement = bytes ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : {};
|
|
325
|
-
}
|
|
326
|
-
catch {
|
|
327
|
-
throw new Error("response_invalid");
|
|
328
|
-
}
|
|
329
|
-
if (!acknowledgement || typeof acknowledgement !== "object" || Array.isArray(acknowledgement))
|
|
330
|
-
throw new Error("response_invalid");
|
|
331
|
-
const envelope = acknowledgement;
|
|
332
|
-
const partial = envelope.partialSuccess ?? envelope.partial_success;
|
|
333
|
-
if (partial !== undefined && partial !== null) {
|
|
334
|
-
if (typeof partial !== "object" || Array.isArray(partial))
|
|
335
|
-
throw new Error("response_invalid");
|
|
336
|
-
const rawRejected = partial.rejectedSpans ?? partial.rejected_spans ?? 0;
|
|
337
|
-
if ((typeof rawRejected !== "number" && typeof rawRejected !== "string") || !/^\d+$/.test(String(rawRejected)))
|
|
338
|
-
throw new Error("response_invalid");
|
|
339
|
-
if (Number(rawRejected) > 0)
|
|
340
|
-
throw new Error("partial_rejection");
|
|
341
|
-
}
|
|
374
|
+
throw Object.assign(new Error(failureKind), { attempts, status });
|
|
342
375
|
};
|
|
343
376
|
const privateExporter = {
|
|
344
377
|
export: (spans, done) => {
|
|
@@ -356,7 +389,9 @@ function createOtelEmitter(env, warn, summary) {
|
|
|
356
389
|
done({ code: 1, error: new Error("langfuse_delivery_failed") });
|
|
357
390
|
summary.failed(spans.length);
|
|
358
391
|
summary.maybeReport();
|
|
359
|
-
|
|
392
|
+
const details = error;
|
|
393
|
+
warn(error, { stage: "export", observation_count: spans.length, failure_kind: failureKind,
|
|
394
|
+
attempts: details.attempts, http_status: details.status });
|
|
360
395
|
});
|
|
361
396
|
pendingExports.add(pending);
|
|
362
397
|
// Even an unexpected SDK callback failure must not create a rejected
|
|
@@ -453,7 +488,23 @@ function createOtelEmitter(env, warn, summary) {
|
|
|
453
488
|
identity = undefined;
|
|
454
489
|
}
|
|
455
490
|
};
|
|
456
|
-
return { processor
|
|
491
|
+
return { processor: {
|
|
492
|
+
// BSP forceFlush drains its queue, but does not wait for a batch its
|
|
493
|
+
// interval already removed. Always drain our active exporter too,
|
|
494
|
+
// including when the queued batch rejects.
|
|
495
|
+
forceFlush: async () => { try {
|
|
496
|
+
await processor.forceFlush();
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
await privateExporter.forceFlush();
|
|
500
|
+
} },
|
|
501
|
+
shutdown: async () => { try {
|
|
502
|
+
await processor.shutdown();
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
await privateExporter.shutdown();
|
|
506
|
+
} },
|
|
507
|
+
}, emit };
|
|
457
508
|
}
|
|
458
509
|
catch (error) {
|
|
459
510
|
warn(error, { stage: "init" });
|
|
@@ -693,6 +744,11 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
693
744
|
}
|
|
694
745
|
return null;
|
|
695
746
|
}
|
|
747
|
+
const deliveryContext = {
|
|
748
|
+
tracing_environment: resolveLlmTracingEnvironment(env),
|
|
749
|
+
worker_role: ["relay", "bulk", "interactive", "provider", "outbound", "maintenance"].includes(env.OXYGEN_WORKER_QUEUE_ROLE ?? "")
|
|
750
|
+
? env.OXYGEN_WORKER_QUEUE_ROLE : undefined,
|
|
751
|
+
};
|
|
696
752
|
const lastWarnAtMs = new Map();
|
|
697
753
|
const warn = (error, context) => {
|
|
698
754
|
const nowMs = Date.now();
|
|
@@ -708,6 +764,7 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
708
764
|
// Exporter/API error messages can contain the rejected request or server
|
|
709
765
|
// response. Only this fixed classification may leave the private store.
|
|
710
766
|
error_code: errorCode,
|
|
767
|
+
...deliveryContext,
|
|
711
768
|
...context,
|
|
712
769
|
});
|
|
713
770
|
};
|
|
@@ -738,7 +795,7 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
738
795
|
return observationSpanId(["langfuse-observation-v1", body.traceId, kind, body.name, body.id,
|
|
739
796
|
body.startTime.toISOString(), body.metadata?.attempt_id ?? null, body.metadata?.call_id ?? null]);
|
|
740
797
|
};
|
|
741
|
-
const summary = createLlmExportSummary();
|
|
798
|
+
const summary = createLlmExportSummary(Date.now, deliveryContext);
|
|
742
799
|
const transport = options?.emitterImpl ?? createOtelEmitter(env, warn, summary);
|
|
743
800
|
const emitter = {
|
|
744
801
|
...transport,
|
|
@@ -970,7 +1027,7 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
970
1027
|
}
|
|
971
1028
|
finally {
|
|
972
1029
|
summary.reportNow();
|
|
973
|
-
} })(), (error) => warn(error, { stage: "shutdown" }),
|
|
1030
|
+
} })(), (error) => warn(error, { stage: "shutdown" }), LANGFUSE_SHUTDOWN_TIMEOUT_MS),
|
|
974
1031
|
};
|
|
975
1032
|
}
|
|
976
1033
|
// --- Process-wide singleton (both runtimes construct at most one client) ------
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const SUMMARY_FIELDS = new Set([
|
|
2
|
+
"id", "runId", "tableId", "tableSlug", "status", "counts", "countsAvailable", "summaryLevel", "progress_counts_available", "creditsUsed", "creditsReserved",
|
|
3
|
+
"createdAt", "updatedAt", "startedAt", "completedAt", "lastError", "action_run_id",
|
|
4
|
+
"workspace_run_id", "table_id", "table_slug", "web_url", "deepLink", "plan",
|
|
5
|
+
"execution_status", "health_status", "user_status", "user_message", "progress_label",
|
|
6
|
+
"next_best_action", "recommended_action", "next_actions", "queue_age_seconds",
|
|
7
|
+
"deferred_count", "retrying_count", "credit_limit_skipped", "throughput", "bottlenecks",
|
|
8
|
+
"provider_errors", "provider_health", "provider_timeout_breaker", "classification_review",
|
|
9
|
+
"worker_health", "queue_backend", "execution_backend", "max_concurrency_requested",
|
|
10
|
+
"max_concurrency_effective", "enrichment_rollup", "idempotent_replay",
|
|
11
|
+
]);
|
|
12
|
+
/** Project the already-authorized, customer-safe native run; no provider or retry effects. */
|
|
13
|
+
export function tableRunSummary(run) {
|
|
14
|
+
if (run.view === "summary")
|
|
15
|
+
return run;
|
|
16
|
+
const included = (key) => SUMMARY_FIELDS.has(key) || /credit|cost|approval|error|failure/i.test(key);
|
|
17
|
+
const id = run.action_run_id ?? run.id;
|
|
18
|
+
return {
|
|
19
|
+
...Object.fromEntries(Object.entries(run).filter(([key]) => included(key))),
|
|
20
|
+
view: "summary",
|
|
21
|
+
omitted_fields: Object.keys(run).filter((key) => !included(key)).sort(),
|
|
22
|
+
full_read: { tool: "oxygen_table_runs_get", arguments: { action_run_id: id, view: "full" },
|
|
23
|
+
gateway: { tool: "oxygen_capabilities_read", arguments: { tool: "oxygen_table_runs_get", arguments: { action_run_id: id, view: "full" } } },
|
|
24
|
+
cli: `oxygen table-runs get ${String(id)} --full --json` },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const OXYGEN_BUILD_VERSION = "1.1014.
|
|
1
|
+
export declare const OXYGEN_BUILD_VERSION = "1.1014.81";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
|
|
2
|
-
export const OXYGEN_BUILD_VERSION = "1.1014.
|
|
2
|
+
export const OXYGEN_BUILD_VERSION = "1.1014.81";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const WORKSPACE_SKILL_MAX_FILES = 50;
|
|
2
|
+
export declare const WORKSPACE_SKILL_MAX_FILE_BYTES = 120000;
|
|
3
|
+
export declare const WORKSPACE_SKILL_MAX_BYTES = 1000000;
|
|
4
|
+
export type WorkspaceSkillFile = {
|
|
5
|
+
path: string;
|
|
6
|
+
content: string;
|
|
7
|
+
};
|
|
8
|
+
export type WorkspaceSkillBundle = {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
files: WorkspaceSkillFile[];
|
|
12
|
+
source_url?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function workspaceSkillPath(path: unknown): string;
|
|
15
|
+
export declare function validateWorkspaceSkillFile(file: WorkspaceSkillFile): number;
|
|
16
|
+
/** Content stays verbatim. Names and paths are metadata, never tool authority. */
|
|
17
|
+
export declare function parseWorkspaceSkillBundle(input: {
|
|
18
|
+
files: readonly WorkspaceSkillFile[];
|
|
19
|
+
name?: string;
|
|
20
|
+
source_url?: string;
|
|
21
|
+
}): WorkspaceSkillBundle;
|
|
22
|
+
export declare function authorWorkspaceSkill(input: {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
instructions: string;
|
|
26
|
+
}): WorkspaceSkillBundle;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { OxygenError } from "./cli-result.js";
|
|
2
|
+
import { parseKnowledgeVaultMarkdown } from "./knowledge-vault-markdown.js";
|
|
3
|
+
export const WORKSPACE_SKILL_MAX_FILES = 50;
|
|
4
|
+
export const WORKSPACE_SKILL_MAX_FILE_BYTES = 120_000;
|
|
5
|
+
export const WORKSPACE_SKILL_MAX_BYTES = 1_000_000;
|
|
6
|
+
function invalid(message) {
|
|
7
|
+
throw new OxygenError("invalid_workspace_skill", message, { exitCode: 2 });
|
|
8
|
+
}
|
|
9
|
+
export function workspaceSkillPath(path) {
|
|
10
|
+
if (typeof path !== "string" ||
|
|
11
|
+
path.length > 240 ||
|
|
12
|
+
!path ||
|
|
13
|
+
/[\\\u0000-\u001f\u007f]/u.test(path) ||
|
|
14
|
+
path
|
|
15
|
+
.split("/")
|
|
16
|
+
.some((part) => !part || part === "." || part === ".." || part.length > 120) ||
|
|
17
|
+
path.split("/").length > 15)
|
|
18
|
+
invalid("File paths must be relative, without empty segments, backslashes or traversal.");
|
|
19
|
+
return path;
|
|
20
|
+
}
|
|
21
|
+
export function validateWorkspaceSkillFile(file) {
|
|
22
|
+
const path = workspaceSkillPath(file?.path);
|
|
23
|
+
if (typeof file.content !== "string" || file.content.includes("\0"))
|
|
24
|
+
invalid(`File ${path} must contain text.`);
|
|
25
|
+
const bytes = new TextEncoder().encode(file.content).length;
|
|
26
|
+
if (bytes > WORKSPACE_SKILL_MAX_FILE_BYTES)
|
|
27
|
+
invalid(`File ${path} exceeds ${WORKSPACE_SKILL_MAX_FILE_BYTES} bytes.`);
|
|
28
|
+
return bytes;
|
|
29
|
+
}
|
|
30
|
+
/** Content stays verbatim. Names and paths are metadata, never tool authority. */
|
|
31
|
+
export function parseWorkspaceSkillBundle(input) {
|
|
32
|
+
if (!Array.isArray(input.files) ||
|
|
33
|
+
input.files.length < 1 ||
|
|
34
|
+
input.files.length > WORKSPACE_SKILL_MAX_FILES)
|
|
35
|
+
invalid(`Provide 1–${WORKSPACE_SKILL_MAX_FILES} text files, including SKILL.md.`);
|
|
36
|
+
const paths = new Set();
|
|
37
|
+
let total = 0;
|
|
38
|
+
for (const file of input.files) {
|
|
39
|
+
const path = workspaceSkillPath(file?.path);
|
|
40
|
+
if (paths.has(path.toLowerCase()))
|
|
41
|
+
invalid(`Duplicate file path: ${path}`);
|
|
42
|
+
paths.add(path.toLowerCase());
|
|
43
|
+
total += validateWorkspaceSkillFile(file);
|
|
44
|
+
}
|
|
45
|
+
for (const path of paths) {
|
|
46
|
+
const parts = path.split("/");
|
|
47
|
+
for (let i = 1; i < parts.length; i++)
|
|
48
|
+
if (paths.has(parts.slice(0, i).join("/")))
|
|
49
|
+
invalid("A skill path cannot be both a file and a directory.");
|
|
50
|
+
}
|
|
51
|
+
if (total > WORKSPACE_SKILL_MAX_BYTES)
|
|
52
|
+
invalid(`Skill exceeds ${WORKSPACE_SKILL_MAX_BYTES} bytes.`);
|
|
53
|
+
const entry = input.files.find((file) => file.path === "SKILL.md");
|
|
54
|
+
if (!entry)
|
|
55
|
+
invalid("The skill must include a root SKILL.md file.");
|
|
56
|
+
const parsed = parseKnowledgeVaultMarkdown(entry.content);
|
|
57
|
+
if (parsed.frontmatterError)
|
|
58
|
+
invalid(`Invalid SKILL.md frontmatter: ${parsed.frontmatterError}`);
|
|
59
|
+
const name = input.name ?? parsed.properties.name;
|
|
60
|
+
const description = parsed.properties.description;
|
|
61
|
+
if (typeof name !== "string" ||
|
|
62
|
+
!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) ||
|
|
63
|
+
name.length > 64)
|
|
64
|
+
invalid("A skill name must contain 1–64 lowercase letters, digits or single hyphens.");
|
|
65
|
+
if (typeof description !== "string" ||
|
|
66
|
+
!description.trim() ||
|
|
67
|
+
description.length > 1_024)
|
|
68
|
+
invalid("SKILL.md needs a description in its YAML frontmatter (1–1024 characters).");
|
|
69
|
+
if (input.source_url !== undefined) {
|
|
70
|
+
let url;
|
|
71
|
+
try {
|
|
72
|
+
url = new URL(input.source_url);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
invalid("source_url must be an HTTPS source URL.");
|
|
76
|
+
}
|
|
77
|
+
if (url.protocol !== "https:" ||
|
|
78
|
+
url.username ||
|
|
79
|
+
url.password ||
|
|
80
|
+
input.source_url.length > 2_000)
|
|
81
|
+
invalid("source_url must be an HTTPS source URL without credentials.");
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
name,
|
|
85
|
+
description: description.trim(),
|
|
86
|
+
files: input.files.map((file) => ({ ...file })),
|
|
87
|
+
...(input.source_url ? { source_url: input.source_url } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function authorWorkspaceSkill(input) {
|
|
91
|
+
if (typeof input.instructions !== "string" || !input.instructions.trim())
|
|
92
|
+
invalid("Skill instructions are required.");
|
|
93
|
+
return parseWorkspaceSkillBundle({
|
|
94
|
+
name: input.name,
|
|
95
|
+
files: [
|
|
96
|
+
{
|
|
97
|
+
path: "SKILL.md",
|
|
98
|
+
content: `---\nname: ${JSON.stringify(input.name)}\ndescription: ${JSON.stringify(input.description)}\n---\n\n${input.instructions}\n`,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
}
|