@desplega.ai/agent-swarm 1.93.0 → 1.94.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/README.md +2 -2
- package/openapi.json +180 -1
- package/package.json +1 -1
- package/src/be/db.ts +63 -7
- package/src/be/migrations/090_model_tiers.sql +2 -0
- package/src/be/migrations/091_seed_swarm_operations_metrics.sql +12 -0
- package/src/be/migrations/092_metrics_dashboard_combobox_filters.sql +68 -0
- package/src/be/migrations/093_slack_message_tracking.sql +6 -0
- package/src/be/migrations/runner.ts +52 -0
- package/src/be/modelsdev-cache.json +2060 -198
- package/src/be/scripts/boot-reembed.ts +74 -0
- package/src/be/scripts/db.ts +19 -3
- package/src/be/seed/index.ts +1 -1
- package/src/be/seed/registry.ts +2 -2
- package/src/be/seed/runner.ts +5 -5
- package/src/be/seed/types.ts +6 -1
- package/src/be/seed-pricing.ts +1 -0
- package/src/be/seed-scripts/index.ts +3 -2
- package/src/commands/runner.ts +83 -13
- package/src/http/index.ts +13 -2
- package/src/http/metrics.ts +55 -6
- package/src/http/schedules.ts +16 -15
- package/src/http/script-runs.ts +7 -1
- package/src/http/scripts.ts +147 -1
- package/src/http/tasks.ts +7 -0
- package/src/model-tiers.ts +140 -0
- package/src/providers/claude-managed-models.ts +9 -0
- package/src/providers/opencode-adapter.ts +1 -0
- package/src/providers/pi-mono-adapter.ts +78 -6
- package/src/scheduler/scheduler.ts +22 -34
- package/src/server-user.ts +8 -2
- package/src/slack/responses.ts +39 -11
- package/src/slack/watcher.ts +121 -8
- package/src/tests/agents-list-model-display.test.ts +13 -0
- package/src/tests/aws-error-classifier.test.ts +148 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -0
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/http-api-integration.test.ts +19 -0
- package/src/tests/metrics-http.test.ts +137 -3
- package/src/tests/migration-046-budgets.test.ts +33 -0
- package/src/tests/migration-runner-regressions.test.ts +69 -0
- package/src/tests/model-control.test.ts +162 -46
- package/src/tests/opencode-adapter.test.ts +9 -0
- package/src/tests/pi-mono-adapter.test.ts +319 -0
- package/src/tests/providers/pi-cost.test.ts +9 -0
- package/src/tests/runner-fallback-output.test.ts +50 -0
- package/src/tests/scripts-boot-reembed.test.ts +163 -0
- package/src/tests/scripts-embeddings.test.ts +90 -0
- package/src/tests/seed.test.ts +26 -1
- package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
- package/src/tests/slack-watcher.test.ts +66 -0
- package/src/tests/workflow-agent-task.test.ts +5 -2
- package/src/tests/workflow-validation-port-routing.test.ts +181 -0
- package/src/tools/memory-get.ts +11 -0
- package/src/tools/memory-search.ts +18 -0
- package/src/tools/schedules/create-schedule.ts +71 -70
- package/src/tools/schedules/update-schedule.ts +43 -31
- package/src/tools/send-task.ts +16 -5
- package/src/tools/task-action.ts +11 -3
- package/src/types.ts +29 -0
- package/src/utils/aws-error-classifier.ts +97 -0
- package/src/utils/context-window.ts +2 -0
- package/src/utils/credentials.test.ts +68 -0
- package/src/utils/credentials.ts +44 -3
- package/src/utils/pretty-print.ts +25 -10
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/executors/agent-task.ts +3 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-listen backfill: embed scripts that are missing embeddings (e.g. after
|
|
3
|
+
* boot seeding with scriptEmbeddingMode: "skip"). Runs once per boot,
|
|
4
|
+
* async/non-blocking, idempotent, no-op when every non-scratch script already
|
|
5
|
+
* has an embedding row.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors the memory boot-reembed pattern (src/be/memory/boot-reembed.ts).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getDb } from "@/be/db";
|
|
11
|
+
import type { ScriptScope } from "@/types";
|
|
12
|
+
import { embedScript } from "./embeddings";
|
|
13
|
+
|
|
14
|
+
type ScriptMissingEmbedding = {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
scope: ScriptScope;
|
|
18
|
+
scopeId: string | null;
|
|
19
|
+
source: string;
|
|
20
|
+
description: string;
|
|
21
|
+
intent: string;
|
|
22
|
+
signatureJson: string;
|
|
23
|
+
argsJsonSchema: string | null;
|
|
24
|
+
contentHash: string;
|
|
25
|
+
version: number;
|
|
26
|
+
isScratch: number;
|
|
27
|
+
typeChecked: number;
|
|
28
|
+
fsMode: "none" | "workspace-rw";
|
|
29
|
+
createdByAgentId: string | null;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
updatedAt: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export async function runBootReembedScripts(): Promise<void> {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
|
|
37
|
+
const missing = db
|
|
38
|
+
.prepare<ScriptMissingEmbedding, []>(
|
|
39
|
+
`SELECT s.* FROM scripts s
|
|
40
|
+
LEFT JOIN script_embeddings e ON e.scriptId = s.id
|
|
41
|
+
WHERE s.isScratch = 0 AND e.scriptId IS NULL`,
|
|
42
|
+
)
|
|
43
|
+
.all();
|
|
44
|
+
|
|
45
|
+
if (missing.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`[boot-reembed-scripts] starting: ${missing.length} scripts missing embeddings`);
|
|
50
|
+
|
|
51
|
+
let embedded = 0;
|
|
52
|
+
let failed = 0;
|
|
53
|
+
|
|
54
|
+
for (const row of missing) {
|
|
55
|
+
try {
|
|
56
|
+
await embedScript({
|
|
57
|
+
...row,
|
|
58
|
+
scopeId: row.scopeId ?? null,
|
|
59
|
+
isScratch: row.isScratch === 1,
|
|
60
|
+
typeChecked: row.typeChecked === 1,
|
|
61
|
+
createdByAgentId: row.createdByAgentId ?? null,
|
|
62
|
+
});
|
|
63
|
+
embedded++;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
failed++;
|
|
66
|
+
console.error(
|
|
67
|
+
`[boot-reembed-scripts] failed to embed "${row.name}":`,
|
|
68
|
+
(err as Error).message,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(`[boot-reembed-scripts] complete: embedded=${embedded} failed=${failed}`);
|
|
74
|
+
}
|
package/src/be/scripts/db.ts
CHANGED
|
@@ -26,6 +26,7 @@ type ScriptWriteArgs = ScriptIdentity & {
|
|
|
26
26
|
fsMode?: ScriptFsMode;
|
|
27
27
|
agentId?: string | null;
|
|
28
28
|
changeReason?: string | null;
|
|
29
|
+
embeddingMode?: "sync" | "skip";
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export type UpsertScriptResult = {
|
|
@@ -178,10 +179,11 @@ export function insertScript(args: ScriptWriteArgs): ScriptRecord {
|
|
|
178
179
|
* immediately consistent for authored/promoted scripts.
|
|
179
180
|
*/
|
|
180
181
|
export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertScriptResult> {
|
|
182
|
+
const shouldEmbed = args.embeddingMode !== "skip";
|
|
181
183
|
const existing = getScript(args);
|
|
182
184
|
if (!existing) {
|
|
183
185
|
const script = insertScript(args);
|
|
184
|
-
if (!script.isScratch) {
|
|
186
|
+
if (!script.isScratch && shouldEmbed) {
|
|
185
187
|
await embedScript(script);
|
|
186
188
|
}
|
|
187
189
|
return {
|
|
@@ -235,7 +237,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
|
|
|
235
237
|
|
|
236
238
|
if (!row) throw new Error("Failed to update script metadata");
|
|
237
239
|
const script = rowToScript(row);
|
|
238
|
-
if (!script.isScratch && (trackedMetadataChanged || promotedFromScratch)) {
|
|
240
|
+
if (!script.isScratch && shouldEmbed && (trackedMetadataChanged || promotedFromScratch)) {
|
|
239
241
|
await embedScript(script);
|
|
240
242
|
}
|
|
241
243
|
return {
|
|
@@ -318,7 +320,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
|
|
|
318
320
|
});
|
|
319
321
|
|
|
320
322
|
const script = txn();
|
|
321
|
-
if (!script.isScratch) {
|
|
323
|
+
if (!script.isScratch && shouldEmbed) {
|
|
322
324
|
await embedScript(script);
|
|
323
325
|
}
|
|
324
326
|
|
|
@@ -347,6 +349,11 @@ export function getScript(args: ScriptIdentity): ScriptRecord | null {
|
|
|
347
349
|
return row ? rowToScript(row) : null;
|
|
348
350
|
}
|
|
349
351
|
|
|
352
|
+
export function getScriptById(id: string): ScriptRecord | null {
|
|
353
|
+
const row = getDb().prepare<ScriptRow, [string]>("SELECT * FROM scripts WHERE id = ?").get(id);
|
|
354
|
+
return row ? rowToScript(row) : null;
|
|
355
|
+
}
|
|
356
|
+
|
|
350
357
|
export function getScriptVersion(args: {
|
|
351
358
|
scriptId: string;
|
|
352
359
|
version?: number;
|
|
@@ -408,6 +415,15 @@ export function listScripts(args?: {
|
|
|
408
415
|
.map(rowToScript);
|
|
409
416
|
}
|
|
410
417
|
|
|
418
|
+
export function listScriptVersions(scriptId: string): ScriptVersionRecord[] {
|
|
419
|
+
return getDb()
|
|
420
|
+
.prepare<ScriptVersionRow, [string]>(
|
|
421
|
+
"SELECT * FROM script_versions WHERE scriptId = ? ORDER BY version DESC",
|
|
422
|
+
)
|
|
423
|
+
.all(scriptId)
|
|
424
|
+
.map(rowToScriptVersion);
|
|
425
|
+
}
|
|
426
|
+
|
|
411
427
|
export function deleteScript(args: ScriptIdentity): boolean {
|
|
412
428
|
const existing = getScript(args);
|
|
413
429
|
if (!existing) return false;
|
package/src/be/seed/index.ts
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
export { runAllSeeders, SEEDERS } from "./registry";
|
|
7
7
|
export { runSeeder, runSeeders } from "./runner";
|
|
8
8
|
export { getSeedState, recordSeedState } from "./state-db";
|
|
9
|
-
export type { SeedAction, Seeder, SeederResult, SeedItem } from "./types";
|
|
9
|
+
export type { SeedAction, Seeder, SeederResult, SeederRunOptions, SeedItem } from "./types";
|
package/src/be/seed/registry.ts
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
import { scriptsSeeder } from "../seed-scripts";
|
|
10
10
|
import { skillsSeeder } from "../seed-skills";
|
|
11
11
|
import { runSeeders } from "./runner";
|
|
12
|
-
import type { Seeder, SeederResult } from "./types";
|
|
12
|
+
import type { Seeder, SeederResult, SeederRunOptions } from "./types";
|
|
13
13
|
|
|
14
14
|
export const SEEDERS: Seeder[] = [scriptsSeeder, skillsSeeder];
|
|
15
15
|
|
|
16
16
|
/** Apply every registered seeder. Called at API boot and by the seed CLI. */
|
|
17
|
-
export function runAllSeeders(opts?:
|
|
17
|
+
export function runAllSeeders(opts?: SeederRunOptions): Promise<SeederResult[]> {
|
|
18
18
|
return runSeeders(SEEDERS, opts);
|
|
19
19
|
}
|
package/src/be/seed/runner.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getSeedState, recordSeedState } from "./state-db";
|
|
8
|
-
import type { Seeder, SeederResult } from "./types";
|
|
8
|
+
import type { Seeder, SeederResult, SeederRunOptions } from "./types";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Apply one seeder. Idempotent and version-aware:
|
|
@@ -14,7 +14,7 @@ import type { Seeder, SeederResult } from "./types";
|
|
|
14
14
|
* - upstream pristine, src same -> no-op
|
|
15
15
|
* - upstream user-modified -> preserve (never overwrite)
|
|
16
16
|
*/
|
|
17
|
-
export async function runSeeder(seeder: Seeder, opts?:
|
|
17
|
+
export async function runSeeder(seeder: Seeder, opts?: SeederRunOptions): Promise<SeederResult> {
|
|
18
18
|
const result: SeederResult = {
|
|
19
19
|
kind: seeder.kind,
|
|
20
20
|
created: 0,
|
|
@@ -31,7 +31,7 @@ export async function runSeeder(seeder: Seeder, opts?: { quiet?: boolean }): Pro
|
|
|
31
31
|
|
|
32
32
|
// Absent upstream -> create.
|
|
33
33
|
if (upstream === null) {
|
|
34
|
-
await seeder.apply(item, "create");
|
|
34
|
+
await seeder.apply(item, "create", opts);
|
|
35
35
|
recordSeedState(seeder.kind, item.key, item.contentHash);
|
|
36
36
|
result.created += 1;
|
|
37
37
|
continue;
|
|
@@ -60,7 +60,7 @@ export async function runSeeder(seeder: Seeder, opts?: { quiet?: boolean }): Pro
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// Pristine upstream + changed source -> update to the new source version.
|
|
63
|
-
await seeder.apply(item, "update");
|
|
63
|
+
await seeder.apply(item, "update", opts);
|
|
64
64
|
recordSeedState(seeder.kind, item.key, item.contentHash);
|
|
65
65
|
result.updated += 1;
|
|
66
66
|
} catch (err) {
|
|
@@ -88,7 +88,7 @@ export async function runSeeder(seeder: Seeder, opts?: { quiet?: boolean }): Pro
|
|
|
88
88
|
/** Apply a list of seeders in order. */
|
|
89
89
|
export async function runSeeders(
|
|
90
90
|
seeders: Seeder[],
|
|
91
|
-
opts?:
|
|
91
|
+
opts?: SeederRunOptions,
|
|
92
92
|
): Promise<SeederResult[]> {
|
|
93
93
|
const results: SeederResult[] = [];
|
|
94
94
|
for (const seeder of seeders) {
|
package/src/be/seed/types.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface SeedItem {
|
|
|
35
35
|
readonly contentHash: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export type SeederRunOptions = {
|
|
39
|
+
quiet?: boolean;
|
|
40
|
+
scriptEmbeddingMode?: "sync" | "skip";
|
|
41
|
+
};
|
|
42
|
+
|
|
38
43
|
export interface Seeder<TItem extends SeedItem = SeedItem> {
|
|
39
44
|
/** Kind discriminator — namespaces this seeder's rows in `seed_state`. */
|
|
40
45
|
readonly kind: string;
|
|
@@ -46,7 +51,7 @@ export interface Seeder<TItem extends SeedItem = SeedItem> {
|
|
|
46
51
|
*/
|
|
47
52
|
upstreamHash(item: TItem): string | null | Promise<string | null>;
|
|
48
53
|
/** Create or update the upstream entity so it matches the source definition. */
|
|
49
|
-
apply(item: TItem, action: "create" | "update"): void | Promise<void>;
|
|
54
|
+
apply(item: TItem, action: "create" | "update", opts?: SeederRunOptions): void | Promise<void>;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
export type SeederResult = {
|
package/src/be/seed-pricing.ts
CHANGED
|
@@ -68,6 +68,7 @@ const MANUAL_PRICING_OVERRIDES: Array<{
|
|
|
68
68
|
*/
|
|
69
69
|
const ANTHROPIC_SHORTNAME_TO_MODELSDEV: Record<string, string> = {
|
|
70
70
|
fable: "claude-fable-5",
|
|
71
|
+
mythos: "claude-mythos-5",
|
|
71
72
|
opus: "claude-opus-4-8",
|
|
72
73
|
sonnet: "claude-sonnet-4-6",
|
|
73
74
|
haiku: "claude-haiku-4-5",
|
|
@@ -21,7 +21,7 @@ import { computeContentHash } from "../db";
|
|
|
21
21
|
import { getScript, upsertScriptByName } from "../scripts/db";
|
|
22
22
|
import { extractArgsJsonSchema } from "../scripts/extract-schema";
|
|
23
23
|
import { typecheckScript } from "../scripts/typecheck";
|
|
24
|
-
import type { Seeder, SeedItem } from "../seed/types";
|
|
24
|
+
import type { Seeder, SeederRunOptions, SeedItem } from "../seed/types";
|
|
25
25
|
import bootTriageSrc from "./catalog/boot-triage.inline.ts" with { type: "text" };
|
|
26
26
|
// @ts-expect-error Bun text imports synthesize a default string for this helper.
|
|
27
27
|
import catalogReportSrc from "./catalog/catalog-report.inline.ts" with { type: "text" };
|
|
@@ -234,7 +234,7 @@ export const scriptsSeeder: Seeder<ScriptSeedItem> = {
|
|
|
234
234
|
return existing ? existing.contentHash : null;
|
|
235
235
|
},
|
|
236
236
|
|
|
237
|
-
async apply(item): Promise<void> {
|
|
237
|
+
async apply(item, _action, opts?: SeederRunOptions): Promise<void> {
|
|
238
238
|
const { script } = item;
|
|
239
239
|
|
|
240
240
|
const imports = validateScriptImports(script.source);
|
|
@@ -260,6 +260,7 @@ export const scriptsSeeder: Seeder<ScriptSeedItem> = {
|
|
|
260
260
|
isScratch: false,
|
|
261
261
|
typeChecked: true,
|
|
262
262
|
changeReason: "Seeded from the built-in scripts catalog (src/be/seed-scripts)",
|
|
263
|
+
embeddingMode: opts?.scriptEmbeddingMode ?? "sync",
|
|
263
264
|
});
|
|
264
265
|
},
|
|
265
266
|
};
|
package/src/commands/runner.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, statSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { ensure, initialize } from "@desplega.ai/business-use";
|
|
4
4
|
import type { TemplateResponse } from "../../templates/schema.ts";
|
|
5
|
+
import { resolveTaskModelSelection } from "../model-tiers.ts";
|
|
5
6
|
import {
|
|
6
7
|
type Attributes,
|
|
7
8
|
initOtel,
|
|
@@ -350,6 +351,7 @@ async function fetchResolvedEnv(
|
|
|
350
351
|
apiKey: string,
|
|
351
352
|
agentId: string,
|
|
352
353
|
baseEnv: Record<string, string | undefined> = process.env,
|
|
354
|
+
taskModel?: string,
|
|
353
355
|
): Promise<ResolvedEnvResult> {
|
|
354
356
|
const env: Record<string, string | undefined> = { ...baseEnv };
|
|
355
357
|
|
|
@@ -382,6 +384,12 @@ async function fetchResolvedEnv(
|
|
|
382
384
|
|
|
383
385
|
const resolvedProvider = resolveHarnessProvider(env, baseEnv);
|
|
384
386
|
|
|
387
|
+
// Effective model: per-task model takes priority over the agent-level
|
|
388
|
+
// MODEL_OVERRIDE from swarm_config. Passed to resolveCredentialPools so
|
|
389
|
+
// the harness × model matrix can exclude incompatible credential vars
|
|
390
|
+
// (e.g. OPENAI_API_KEY when an OpenRouter model is selected on opencode).
|
|
391
|
+
const effectiveModel = taskModel || (env.MODEL_OVERRIDE as string | undefined) || "";
|
|
392
|
+
|
|
385
393
|
const credentialSelections = await resolveCredentialPools(env, {
|
|
386
394
|
apiUrl,
|
|
387
395
|
apiKey,
|
|
@@ -393,6 +401,7 @@ async function fetchResolvedEnv(
|
|
|
393
401
|
// Use the resolved provider (swarm_config > env) so an operator can flip
|
|
394
402
|
// the worker's harness from the dashboard without restarting the container.
|
|
395
403
|
provider: resolvedProvider,
|
|
404
|
+
model: effectiveModel,
|
|
396
405
|
});
|
|
397
406
|
|
|
398
407
|
return { env, credentialSelections, resolvedProvider };
|
|
@@ -867,6 +876,7 @@ export async function ensureTaskFinished(
|
|
|
867
876
|
* from the resolved swarm_config value. Falls back to env when omitted.
|
|
868
877
|
*/
|
|
869
878
|
provider?: ProviderName,
|
|
879
|
+
failureDiagnostics?: string,
|
|
870
880
|
): Promise<void> {
|
|
871
881
|
const headers: Record<string, string> = {
|
|
872
882
|
"X-Agent-ID": config.agentId,
|
|
@@ -883,6 +893,9 @@ export async function ensureTaskFinished(
|
|
|
883
893
|
|
|
884
894
|
if (status === "failed") {
|
|
885
895
|
body.failureReason = failureReason || `Claude process exited with code ${exitCode}`;
|
|
896
|
+
if (failureDiagnostics) {
|
|
897
|
+
body.failureReason = `${body.failureReason}\n\n${failureDiagnostics}`;
|
|
898
|
+
}
|
|
886
899
|
} else if (providerOutput) {
|
|
887
900
|
const validation = await validateProviderOutputIfNeeded(config, taskId, providerOutput);
|
|
888
901
|
if (validation.ok) {
|
|
@@ -1653,6 +1666,32 @@ async function findBridgeFailureArtifact(cwd: string): Promise<string | undefine
|
|
|
1653
1666
|
}
|
|
1654
1667
|
}
|
|
1655
1668
|
|
|
1669
|
+
async function readBridgeFailureTail(
|
|
1670
|
+
artifactPath: string,
|
|
1671
|
+
maxLines = 40,
|
|
1672
|
+
maxChars = 4000,
|
|
1673
|
+
): Promise<string | undefined> {
|
|
1674
|
+
try {
|
|
1675
|
+
const text = await Bun.file(artifactPath).text();
|
|
1676
|
+
const tail = text.split(/\r?\n/).slice(-maxLines).join("\n").trim();
|
|
1677
|
+
if (!tail) return undefined;
|
|
1678
|
+
return tail.length > maxChars ? tail.slice(-maxChars) : tail;
|
|
1679
|
+
} catch {
|
|
1680
|
+
return undefined;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
export async function getBridgeFailureDiagnostics(
|
|
1685
|
+
cwd: string,
|
|
1686
|
+
): Promise<{ artifactPath: string; paneTail?: string } | undefined> {
|
|
1687
|
+
const artifactPath = await findBridgeFailureArtifact(cwd);
|
|
1688
|
+
if (!artifactPath) return undefined;
|
|
1689
|
+
return {
|
|
1690
|
+
artifactPath,
|
|
1691
|
+
paneTail: await readBridgeFailureTail(artifactPath),
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1656
1695
|
async function updateHarnessVariantMeta(
|
|
1657
1696
|
apiUrl: string,
|
|
1658
1697
|
apiKey: string,
|
|
@@ -2519,6 +2558,7 @@ async function spawnProviderProcess(
|
|
|
2519
2558
|
iteration: number;
|
|
2520
2559
|
taskId?: string;
|
|
2521
2560
|
model?: string;
|
|
2561
|
+
modelTier?: string;
|
|
2522
2562
|
resumeSessionId?: string;
|
|
2523
2563
|
harnessProvider: ProviderName;
|
|
2524
2564
|
cwd?: string;
|
|
@@ -2532,11 +2572,15 @@ async function spawnProviderProcess(
|
|
|
2532
2572
|
// Correlation ID for logs/display — always defined
|
|
2533
2573
|
const effectiveTaskId = realTaskId || crypto.randomUUID();
|
|
2534
2574
|
|
|
2535
|
-
// Resolve env first so we can use MODEL_OVERRIDE from config
|
|
2575
|
+
// Resolve env first so we can use MODEL_OVERRIDE from config.
|
|
2576
|
+
// Pass opts.model (per-task model) so the credential picker can apply
|
|
2577
|
+
// the harness × model matrix (e.g. exclude OPENAI_API_KEY for OpenRouter models).
|
|
2536
2578
|
const { env: freshEnv, credentialSelections } = await fetchResolvedEnv(
|
|
2537
2579
|
opts.apiUrl,
|
|
2538
2580
|
opts.apiKey,
|
|
2539
2581
|
opts.agentId,
|
|
2582
|
+
process.env,
|
|
2583
|
+
opts.model,
|
|
2540
2584
|
);
|
|
2541
2585
|
|
|
2542
2586
|
// Report which key was selected for this task (fire-and-forget)
|
|
@@ -2553,7 +2597,14 @@ async function spawnProviderProcess(
|
|
|
2553
2597
|
}
|
|
2554
2598
|
|
|
2555
2599
|
const configModel = (freshEnv.MODEL_OVERRIDE as string | undefined) || "";
|
|
2556
|
-
const
|
|
2600
|
+
const taskModelSelection = resolveTaskModelSelection({
|
|
2601
|
+
model: opts.model,
|
|
2602
|
+
modelTier: opts.modelTier,
|
|
2603
|
+
harnessProvider: opts.harnessProvider,
|
|
2604
|
+
env: freshEnv,
|
|
2605
|
+
});
|
|
2606
|
+
const taskModel = taskModelSelection.model || "";
|
|
2607
|
+
const model = taskModel || configModel || "";
|
|
2557
2608
|
|
|
2558
2609
|
// Resolve Codex OAuth pool slot BEFORE building ProviderSessionConfig so we
|
|
2559
2610
|
// can pass codexSlot through and the adapter writes token refreshes back to
|
|
@@ -2644,7 +2695,7 @@ async function spawnProviderProcess(
|
|
|
2644
2695
|
);
|
|
2645
2696
|
const initialModelReport = buildLatestModelReport({
|
|
2646
2697
|
model,
|
|
2647
|
-
taskModel
|
|
2698
|
+
taskModel,
|
|
2648
2699
|
configModel,
|
|
2649
2700
|
taskId: realTaskId,
|
|
2650
2701
|
harnessProvider: opts.harnessProvider,
|
|
@@ -2766,6 +2817,17 @@ async function spawnProviderProcess(
|
|
|
2766
2817
|
);
|
|
2767
2818
|
}
|
|
2768
2819
|
|
|
2820
|
+
// Structured session-start log for observability (covers all providers)
|
|
2821
|
+
{
|
|
2822
|
+
const variant = event.harnessVariant ?? "unknown";
|
|
2823
|
+
const version =
|
|
2824
|
+
(event.harnessVariantMeta as Record<string, unknown> | undefined)?.version ??
|
|
2825
|
+
"unknown";
|
|
2826
|
+
console.log(
|
|
2827
|
+
`[${opts.role}] [harness] provider=${event.provider ?? opts.harnessProvider} variant=${variant} version=${version} model=${model || "default"}`,
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2769
2831
|
// Buffer session start event
|
|
2770
2832
|
bufferEvent({
|
|
2771
2833
|
category: "session",
|
|
@@ -3342,6 +3404,20 @@ async function checkCompletedProcesses(
|
|
|
3342
3404
|
rateLimitedUntil,
|
|
3343
3405
|
).catch(() => {});
|
|
3344
3406
|
}
|
|
3407
|
+
let bridgeDiagnostics: Awaited<ReturnType<typeof getBridgeFailureDiagnostics>> | undefined;
|
|
3408
|
+
if (result.exitCode !== 0 && harnessProvider === "claude" && workingDir) {
|
|
3409
|
+
bridgeDiagnostics = await getBridgeFailureDiagnostics(workingDir);
|
|
3410
|
+
if (bridgeDiagnostics?.artifactPath && result.sessionId) {
|
|
3411
|
+
console.log(`[${role}] Bridge failure artifact found: ${bridgeDiagnostics.artifactPath}`);
|
|
3412
|
+
updateHarnessVariantMeta(apiConfig.apiUrl, apiConfig.apiKey, taskId, result.sessionId, {
|
|
3413
|
+
failureArtifact: bridgeDiagnostics.artifactPath,
|
|
3414
|
+
}).catch((err) => console.warn(`[runner] Failed to update harness variant meta: ${err}`));
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
const bridgeFailureDiagnostics =
|
|
3418
|
+
bridgeDiagnostics?.paneTail != null
|
|
3419
|
+
? `Claude bridge final tmux pane tail (${bridgeDiagnostics.artifactPath}):\n${bridgeDiagnostics.paneTail}`
|
|
3420
|
+
: undefined;
|
|
3345
3421
|
await ensureTaskFinished(
|
|
3346
3422
|
apiConfig,
|
|
3347
3423
|
role,
|
|
@@ -3350,6 +3426,7 @@ async function checkCompletedProcesses(
|
|
|
3350
3426
|
failureReason,
|
|
3351
3427
|
result.output,
|
|
3352
3428
|
harnessProvider,
|
|
3429
|
+
bridgeFailureDiagnostics,
|
|
3353
3430
|
);
|
|
3354
3431
|
|
|
3355
3432
|
if (result.exitCode === 0 && credentialInfo) {
|
|
@@ -3361,16 +3438,6 @@ async function checkCompletedProcesses(
|
|
|
3361
3438
|
).catch(() => {});
|
|
3362
3439
|
}
|
|
3363
3440
|
|
|
3364
|
-
if (result.exitCode !== 0 && harnessProvider === "claude" && workingDir && result.sessionId) {
|
|
3365
|
-
const artifactPath = await findBridgeFailureArtifact(workingDir);
|
|
3366
|
-
if (artifactPath) {
|
|
3367
|
-
console.log(`[${role}] Bridge failure artifact found: ${artifactPath}`);
|
|
3368
|
-
updateHarnessVariantMeta(apiConfig.apiUrl, apiConfig.apiKey, taskId, result.sessionId, {
|
|
3369
|
-
failureArtifact: artifactPath,
|
|
3370
|
-
}).catch((err) => console.warn(`[runner] Failed to update harness variant meta: ${err}`));
|
|
3371
|
-
}
|
|
3372
|
-
}
|
|
3373
|
-
|
|
3374
3441
|
ensure({
|
|
3375
3442
|
id: "worker_process_finished",
|
|
3376
3443
|
flow: "task",
|
|
@@ -4391,6 +4458,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4391
4458
|
iteration,
|
|
4392
4459
|
taskId: task.id,
|
|
4393
4460
|
model: (task as { model?: string }).model,
|
|
4461
|
+
modelTier: (task as { modelTier?: string }).modelTier,
|
|
4394
4462
|
harnessProvider: state.harnessProvider,
|
|
4395
4463
|
cwd: resumeCwd,
|
|
4396
4464
|
vcsRepo: task.vcsRepo,
|
|
@@ -4710,6 +4778,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4710
4778
|
|
|
4711
4779
|
// Extract model from task data for per-task model selection
|
|
4712
4780
|
const taskModel = (trigger.task as { model?: string } | undefined)?.model;
|
|
4781
|
+
const taskModelTier = (trigger.task as { modelTier?: string } | undefined)?.modelTier;
|
|
4713
4782
|
|
|
4714
4783
|
// Detect Slack context for conditional prompt sections
|
|
4715
4784
|
const taskSlackChannelId = (trigger.task as { slackChannelId?: string } | undefined)
|
|
@@ -4852,6 +4921,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4852
4921
|
iteration,
|
|
4853
4922
|
taskId: trigger.taskId,
|
|
4854
4923
|
model: taskModel,
|
|
4924
|
+
modelTier: taskModelTier,
|
|
4855
4925
|
harnessProvider: state.harnessProvider,
|
|
4856
4926
|
cwd: effectiveCwd,
|
|
4857
4927
|
vcsRepo: taskVcsRepo,
|
package/src/http/index.ts
CHANGED
|
@@ -458,10 +458,12 @@ try {
|
|
|
458
458
|
// Seed the built-in entity catalog (scripts today; more kinds later) so
|
|
459
459
|
// `script-search` & co. return useful hits from a fresh DB. Idempotent and
|
|
460
460
|
// version-aware: a pristine entity updates when its source changes, a
|
|
461
|
-
// user-modified one is preserved.
|
|
461
|
+
// user-modified one is preserved. Script embeddings are deferred to a
|
|
462
|
+
// post-listen backfill so boot doesn't block on embedding provider calls.
|
|
463
|
+
// See src/be/seed for the framework.
|
|
462
464
|
try {
|
|
463
465
|
const { runAllSeeders } = await import("../be/seed");
|
|
464
|
-
await runAllSeeders();
|
|
466
|
+
await runAllSeeders({ scriptEmbeddingMode: "skip" });
|
|
465
467
|
} catch (err) {
|
|
466
468
|
console.error("[startup] Failed to seed built-in entities:", err);
|
|
467
469
|
}
|
|
@@ -565,6 +567,15 @@ httpServer
|
|
|
565
567
|
.catch((err) => {
|
|
566
568
|
console.error("[boot-reembed] startup backfill failed (non-fatal):", err);
|
|
567
569
|
});
|
|
570
|
+
|
|
571
|
+
// Background backfill: embed any scripts that were seeded without embeddings
|
|
572
|
+
// (scriptEmbeddingMode: "skip" during boot). Non-blocking, idempotent, no-op
|
|
573
|
+
// when every non-scratch script already has an embedding.
|
|
574
|
+
import("../be/scripts/boot-reembed")
|
|
575
|
+
.then(({ runBootReembedScripts }) => runBootReembedScripts())
|
|
576
|
+
.catch((err) => {
|
|
577
|
+
console.error("[boot-reembed-scripts] startup backfill failed (non-fatal):", err);
|
|
578
|
+
});
|
|
568
579
|
})
|
|
569
580
|
.on("error", (err) => {
|
|
570
581
|
console.error("HTTP Server Error:", err);
|
package/src/http/metrics.ts
CHANGED
|
@@ -48,12 +48,48 @@ function slugify(input: string): string {
|
|
|
48
48
|
|
|
49
49
|
function validateMetricDefinition(definition: unknown) {
|
|
50
50
|
const parsed = MetricDefinitionSchema.parse(definition);
|
|
51
|
+
for (const variable of parsed.variables ?? []) {
|
|
52
|
+
if (variable.optionsQuery) {
|
|
53
|
+
assertSelectOnlyQuery(variable.optionsQuery.sql);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
51
56
|
for (const widget of parsed.widgets) {
|
|
52
57
|
assertSelectOnlyQuery(widget.query.sql);
|
|
53
58
|
}
|
|
54
59
|
return parsed;
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
function resolveVariableOptionValues(variable: MetricVariable) {
|
|
63
|
+
if (!variable.optionsQuery) return variable.options ?? [];
|
|
64
|
+
assertSelectOnlyQuery(variable.optionsQuery.sql);
|
|
65
|
+
const result = executeReadOnlyQuery(variable.optionsQuery.sql, [], HARD_METRIC_MAX_ROWS);
|
|
66
|
+
return result.rows.map((row) => {
|
|
67
|
+
const record = Object.fromEntries(
|
|
68
|
+
result.columns.map((column, index) => [column, row[index] as MetricParam]),
|
|
69
|
+
);
|
|
70
|
+
const value = record[variable.optionsQuery!.valueKey];
|
|
71
|
+
const labelKey = variable.optionsQuery!.labelKey ?? variable.optionsQuery!.valueKey;
|
|
72
|
+
const label = record[labelKey] ?? value;
|
|
73
|
+
return {
|
|
74
|
+
label: label == null ? "" : String(label),
|
|
75
|
+
value: value == null ? null : value,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveVariableOptions(metric: Metric) {
|
|
81
|
+
const optionsByKey: Record<string, Array<{ label: string; value: MetricParam }>> = {};
|
|
82
|
+
const variables = (metric.definition.variables ?? []).map((variable) => {
|
|
83
|
+
if (!variable.optionsQuery) {
|
|
84
|
+
return variable;
|
|
85
|
+
}
|
|
86
|
+
const options = resolveVariableOptionValues(variable);
|
|
87
|
+
optionsByKey[variable.key] = options;
|
|
88
|
+
return { ...variable, options };
|
|
89
|
+
});
|
|
90
|
+
return { variables, optionsByKey };
|
|
91
|
+
}
|
|
92
|
+
|
|
57
93
|
function coerceVariableValue(variable: MetricVariable, raw: unknown): MetricParam {
|
|
58
94
|
if (raw == null || raw === "") {
|
|
59
95
|
return variable.defaultValue ?? null;
|
|
@@ -71,12 +107,21 @@ function coerceVariableValue(variable: MetricVariable, raw: unknown): MetricPara
|
|
|
71
107
|
return String(raw);
|
|
72
108
|
}
|
|
73
109
|
|
|
74
|
-
function resolveMetricVariables(
|
|
110
|
+
function resolveMetricVariables(
|
|
111
|
+
metric: Metric,
|
|
112
|
+
provided: Record<string, unknown>,
|
|
113
|
+
dynamicOptionsByKey: Record<string, Array<{ label: string; value: MetricParam }>> = {},
|
|
114
|
+
) {
|
|
75
115
|
const values: Record<string, MetricParam> = {};
|
|
76
116
|
for (const variable of metric.definition.variables ?? []) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
117
|
+
const options = dynamicOptionsByKey[variable.key] ?? variable.options;
|
|
118
|
+
const raw = provided[variable.key];
|
|
119
|
+
const value =
|
|
120
|
+
(raw == null || raw === "") && variable.defaultValue === undefined && options?.length
|
|
121
|
+
? options[0]!.value
|
|
122
|
+
: coerceVariableValue(variable, raw);
|
|
123
|
+
if (options?.length) {
|
|
124
|
+
const allowed = options.some((option) => option.value === value);
|
|
80
125
|
if (!allowed) {
|
|
81
126
|
throw new Error(`Metric variable "${variable.key}" must match one of its options`);
|
|
82
127
|
}
|
|
@@ -125,10 +170,14 @@ function runMetricWidget(widget: MetricWidget, variables: Record<string, MetricP
|
|
|
125
170
|
}
|
|
126
171
|
|
|
127
172
|
function runMetric(metric: Metric, providedVariables: Record<string, unknown> = {}) {
|
|
128
|
-
const
|
|
173
|
+
const resolved = resolveVariableOptions(metric);
|
|
174
|
+
const variables = resolveMetricVariables(metric, providedVariables, resolved.optionsByKey);
|
|
129
175
|
const widgets = metric.definition.widgets.map((widget) => runMetricWidget(widget, variables));
|
|
130
176
|
return {
|
|
131
|
-
metric
|
|
177
|
+
metric: {
|
|
178
|
+
...metric,
|
|
179
|
+
definition: { ...metric.definition, variables: resolved.variables },
|
|
180
|
+
},
|
|
132
181
|
variables,
|
|
133
182
|
widgets,
|
|
134
183
|
// Kept as the first widget result for older callers during the PR cycle.
|
package/src/http/schedules.ts
CHANGED
|
@@ -12,9 +12,8 @@ import {
|
|
|
12
12
|
updateScheduledTask,
|
|
13
13
|
} from "../be/db";
|
|
14
14
|
import { mergeScheduleTiming, validateRecurringTiming } from "../be/schedules/validate";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
15
|
+
import { ModelTierSchema, splitLegacyModelAlias } from "../model-tiers";
|
|
16
|
+
import { calculateNextRun, createStandaloneScheduleTask } from "../scheduler/scheduler";
|
|
18
17
|
import { getExecutorRegistry } from "../workflows";
|
|
19
18
|
import { handleScheduleTrigger } from "../workflows/triggers";
|
|
20
19
|
import { route } from "./route-def";
|
|
@@ -41,6 +40,7 @@ const createSchedule = route({
|
|
|
41
40
|
enabled: z.boolean().optional(),
|
|
42
41
|
timezone: z.string().optional(),
|
|
43
42
|
model: z.string().optional(),
|
|
43
|
+
modelTier: ModelTierSchema.optional(),
|
|
44
44
|
scheduleType: z.enum(["recurring", "one_time"]).optional(),
|
|
45
45
|
delayMs: z.number().int().optional(),
|
|
46
46
|
runAt: z.string().optional(),
|
|
@@ -126,6 +126,7 @@ const updateSchedule = route({
|
|
|
126
126
|
enabled: z.boolean().optional(),
|
|
127
127
|
timezone: z.string().optional(),
|
|
128
128
|
model: z.string().optional(),
|
|
129
|
+
modelTier: ModelTierSchema.nullable().optional(),
|
|
129
130
|
nextRunAt: z.string().nullable().optional(),
|
|
130
131
|
}),
|
|
131
132
|
responses: {
|
|
@@ -270,7 +271,7 @@ export async function handleSchedules(
|
|
|
270
271
|
enabled: body.enabled,
|
|
271
272
|
nextRunAt,
|
|
272
273
|
timezone: body.timezone,
|
|
273
|
-
model: body.model,
|
|
274
|
+
...splitLegacyModelAlias({ model: body.model, modelTier: body.modelTier }),
|
|
274
275
|
scheduleType: body.scheduleType,
|
|
275
276
|
});
|
|
276
277
|
|
|
@@ -333,17 +334,7 @@ export async function handleSchedules(
|
|
|
333
334
|
const now = new Date().toISOString();
|
|
334
335
|
|
|
335
336
|
const task = getDb().transaction(() => {
|
|
336
|
-
const createdTask =
|
|
337
|
-
creatorAgentId: schedule.createdByAgentId,
|
|
338
|
-
taskType: schedule.taskType,
|
|
339
|
-
tags: [...schedule.tags, "scheduled", `schedule:${schedule.name}`, "manual-run"],
|
|
340
|
-
priority: schedule.priority,
|
|
341
|
-
agentId: schedule.targetAgentId,
|
|
342
|
-
model: schedule.model,
|
|
343
|
-
scheduleId: schedule.id,
|
|
344
|
-
source: "schedule",
|
|
345
|
-
contextKey: scheduleContextKey({ scheduleId: schedule.id }),
|
|
346
|
-
});
|
|
337
|
+
const createdTask = createStandaloneScheduleTask(schedule, ["manual-run"]);
|
|
347
338
|
|
|
348
339
|
if (schedule.scheduleType === "one_time") {
|
|
349
340
|
updateScheduledTask(schedule.id, {
|
|
@@ -388,6 +379,16 @@ export async function handleSchedules(
|
|
|
388
379
|
const parsed = await updateSchedule.parse(req, res, pathSegments, queryParams);
|
|
389
380
|
if (!parsed) return true;
|
|
390
381
|
const body = parsed.body as Record<string, unknown>;
|
|
382
|
+
if (parsed.body.model !== undefined || parsed.body.modelTier !== undefined) {
|
|
383
|
+
const normalizedModel = splitLegacyModelAlias({
|
|
384
|
+
model: parsed.body.model,
|
|
385
|
+
modelTier: parsed.body.modelTier,
|
|
386
|
+
});
|
|
387
|
+
if (parsed.body.model !== undefined) body.model = normalizedModel.model ?? null;
|
|
388
|
+
if (parsed.body.modelTier !== undefined || normalizedModel.modelTier) {
|
|
389
|
+
body.modelTier = normalizedModel.modelTier ?? null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
391
392
|
|
|
392
393
|
const existing = getScheduledTaskById(parsed.params.id);
|
|
393
394
|
if (!existing) {
|