@desplega.ai/agent-swarm 1.92.2 → 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 +242 -3
- package/package.json +5 -5
- package/src/be/db.ts +152 -11
- package/src/be/memory/boot-reembed.ts +0 -1
- package/src/be/memory/providers/sqlite-store.ts +42 -25
- package/src/be/memory/raters/llm-client.ts +12 -5
- package/src/be/memory/types.ts +3 -0
- package/src/be/migrations/088_script_runs_list_indexes.sql +10 -0
- package/src/be/migrations/089_harness_variant.sql +2 -0
- 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 +3264 -1166
- 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 +2 -0
- package/src/be/seed-scripts/catalog/boot-triage.inline.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +863 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +506 -0
- package/src/be/seed-scripts/index.ts +8 -7
- package/src/be/skill-sync.ts +28 -179
- package/src/commands/runner.ts +197 -10
- package/src/http/api-keys.ts +42 -0
- package/src/http/index.ts +13 -2
- package/src/http/mcp-bridge.ts +1 -1
- package/src/http/memory.ts +23 -24
- 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 +17 -6
- package/src/model-tiers.ts +140 -0
- package/src/providers/claude-adapter.ts +33 -1
- package/src/providers/claude-managed-adapter.ts +3 -0
- package/src/providers/claude-managed-models.ts +16 -0
- package/src/providers/codex-adapter.ts +8 -1
- package/src/providers/codex-models.ts +1 -0
- package/src/providers/codex-oauth/auth-json.ts +1 -0
- package/src/providers/harness-version.ts +7 -0
- package/src/providers/opencode-adapter.ts +12 -4
- package/src/providers/pi-mono-adapter.ts +90 -8
- package/src/providers/types.ts +2 -0
- package/src/scheduler/scheduler.ts +22 -34
- package/src/scripts-runtime/egress-secrets.ts +83 -0
- package/src/scripts-runtime/eval-harness.ts +4 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/loader.ts +2 -0
- package/src/server-user.ts +8 -2
- package/src/slack/channel-join.ts +41 -0
- package/src/slack/responses.ts +39 -11
- package/src/slack/watcher.ts +121 -8
- package/src/tests/additive-buffer.test.ts +0 -1
- package/src/tests/agents-list-model-display.test.ts +13 -0
- package/src/tests/api-key-tracking.test.ts +113 -0
- package/src/tests/approval-requests.test.ts +0 -6
- package/src/tests/aws-error-classifier.test.ts +148 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -0
- package/src/tests/claude-managed-setup.test.ts +0 -4
- package/src/tests/codex-pool.test.ts +2 -6
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/http-api-integration.test.ts +23 -6
- package/src/tests/memory-edges.test.ts +0 -2
- package/src/tests/memory-rate-endpoint.test.ts +0 -2
- package/src/tests/memory-rater-e2e.test.ts +0 -2
- package/src/tests/memory-store.test.ts +19 -1
- package/src/tests/memory.test.ts +51 -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/reload-config.test.ts +33 -17
- package/src/tests/runner-fallback-output.test.ts +50 -0
- package/src/tests/runner-skills-refresh.test.ts +216 -46
- package/src/tests/script-runs-http.test.ts +7 -1
- package/src/tests/scripts-boot-reembed.test.ts +163 -0
- package/src/tests/scripts-embeddings.test.ts +90 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +129 -0
- package/src/tests/seed-scripts.test.ts +13 -1
- package/src/tests/seed.test.ts +26 -1
- package/src/tests/session-attach.test.ts +6 -6
- package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
- package/src/tests/skill-fs-writer.test.ts +250 -0
- package/src/tests/slack-attachments-block.test.ts +0 -1
- package/src/tests/slack-blocks.test.ts +0 -1
- package/src/tests/slack-channel-join.test.ts +80 -0
- package/src/tests/slack-identity-resolution.test.ts +0 -1
- package/src/tests/slack-watcher.test.ts +66 -0
- package/src/tests/structured-output.test.ts +0 -2
- package/src/tests/use-dismissible-card.test.ts +0 -4
- 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/slack-post.ts +18 -15
- package/src/tools/slack-read.ts +9 -11
- package/src/tools/slack-reply.ts +18 -15
- package/src/tools/slack-start-thread.ts +17 -14
- package/src/tools/task-action.ts +11 -3
- package/src/types.ts +40 -0
- package/src/utils/aws-error-classifier.ts +97 -0
- package/src/utils/context-window.ts +5 -0
- package/src/utils/credentials.test.ts +68 -0
- package/src/utils/credentials.ts +66 -5
- package/src/utils/pretty-print.ts +25 -10
- package/src/utils/skill-fs-writer.ts +220 -0
- package/src/utils/skills-refresh.ts +123 -40
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/executors/agent-task.ts +3 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ProviderName } from "./types";
|
|
3
|
+
|
|
4
|
+
export const ModelTierSchema = z.enum(["smol", "regular", "smart", "ultra"]);
|
|
5
|
+
export type ModelTier = z.infer<typeof ModelTierSchema>;
|
|
6
|
+
|
|
7
|
+
export const MODEL_TIERS = ModelTierSchema.options;
|
|
8
|
+
|
|
9
|
+
export const LEGACY_MODEL_TO_TIER: Record<string, ModelTier> = {
|
|
10
|
+
haiku: "smol",
|
|
11
|
+
sonnet: "regular",
|
|
12
|
+
opus: "smart",
|
|
13
|
+
fable: "ultra",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const MODEL_TIER_LABELS: Record<ModelTier, string> = {
|
|
17
|
+
smol: "Smol",
|
|
18
|
+
regular: "Regular",
|
|
19
|
+
smart: "Smart",
|
|
20
|
+
ultra: "Ultra",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_MODEL_TIER_MAP: Record<ProviderName, Record<ModelTier, string>> = {
|
|
24
|
+
claude: {
|
|
25
|
+
smol: "haiku",
|
|
26
|
+
regular: "sonnet",
|
|
27
|
+
smart: "opus",
|
|
28
|
+
ultra: "fable",
|
|
29
|
+
},
|
|
30
|
+
"claude-managed": {
|
|
31
|
+
smol: "claude-haiku-4-5",
|
|
32
|
+
regular: "claude-sonnet-4-6",
|
|
33
|
+
smart: "claude-opus-4-8",
|
|
34
|
+
ultra: "claude-fable-5",
|
|
35
|
+
},
|
|
36
|
+
codex: {
|
|
37
|
+
smol: "gpt-5.4-mini",
|
|
38
|
+
regular: "gpt-5.4",
|
|
39
|
+
smart: "gpt-5.5",
|
|
40
|
+
ultra: "gpt-5.5",
|
|
41
|
+
},
|
|
42
|
+
pi: {
|
|
43
|
+
smol: "openrouter/deepseek/deepseek-v4-flash",
|
|
44
|
+
regular: "openrouter/deepseek/deepseek-v4-flash",
|
|
45
|
+
smart: "openrouter/deepseek/deepseek-v4-pro",
|
|
46
|
+
ultra: "openrouter/anthropic/claude-opus-4.8",
|
|
47
|
+
},
|
|
48
|
+
opencode: {
|
|
49
|
+
smol: "openrouter/deepseek/deepseek-v4-flash",
|
|
50
|
+
regular: "openrouter/deepseek/deepseek-v4-flash",
|
|
51
|
+
smart: "openrouter/deepseek/deepseek-v4-pro",
|
|
52
|
+
ultra: "openrouter/anthropic/claude-opus-4.8",
|
|
53
|
+
},
|
|
54
|
+
devin: {
|
|
55
|
+
smol: "devin",
|
|
56
|
+
regular: "devin",
|
|
57
|
+
smart: "devin",
|
|
58
|
+
ultra: "devin",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function parseModelTier(value: string | null | undefined): ModelTier | undefined {
|
|
63
|
+
if (!value) return undefined;
|
|
64
|
+
const normalized = value.trim().toLowerCase();
|
|
65
|
+
return ModelTierSchema.safeParse(normalized).success
|
|
66
|
+
? (normalized as ModelTier)
|
|
67
|
+
: LEGACY_MODEL_TO_TIER[normalized];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function splitLegacyModelAlias(input: {
|
|
71
|
+
model?: string | null;
|
|
72
|
+
modelTier?: string | null;
|
|
73
|
+
}): { model?: string; modelTier?: ModelTier } {
|
|
74
|
+
const explicitTier = parseModelTier(input.modelTier);
|
|
75
|
+
const model = input.model?.trim();
|
|
76
|
+
if (!model) return { modelTier: explicitTier };
|
|
77
|
+
|
|
78
|
+
const legacyTier = parseModelTier(model);
|
|
79
|
+
if (legacyTier && !explicitTier) {
|
|
80
|
+
return { modelTier: legacyTier };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
model,
|
|
85
|
+
modelTier: explicitTier,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseTierMapJson(value: string | undefined): Partial<Record<ModelTier, string>> {
|
|
90
|
+
if (!value) return {};
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(value) as unknown;
|
|
93
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
94
|
+
const result: Partial<Record<ModelTier, string>> = {};
|
|
95
|
+
for (const tier of MODEL_TIERS) {
|
|
96
|
+
const model = (parsed as Record<string, unknown>)[tier];
|
|
97
|
+
if (typeof model === "string" && model.trim()) result[tier] = model.trim();
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
} catch {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveModelTier(opts: {
|
|
106
|
+
tier?: string | null;
|
|
107
|
+
harnessProvider: ProviderName;
|
|
108
|
+
env?: Record<string, string | undefined>;
|
|
109
|
+
}): string | undefined {
|
|
110
|
+
const tier = parseModelTier(opts.tier);
|
|
111
|
+
if (!tier) return undefined;
|
|
112
|
+
|
|
113
|
+
const env = opts.env ?? {};
|
|
114
|
+
const jsonOverrides = parseTierMapJson(env.MODEL_TIER_MAP);
|
|
115
|
+
const envKey = `MODEL_TIER_${tier.toUpperCase()}`;
|
|
116
|
+
const directOverride = env[envKey]?.trim();
|
|
117
|
+
if (directOverride) return directOverride;
|
|
118
|
+
if (jsonOverrides[tier]) return jsonOverrides[tier];
|
|
119
|
+
|
|
120
|
+
return DEFAULT_MODEL_TIER_MAP[opts.harnessProvider]?.[tier];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function resolveTaskModelSelection(opts: {
|
|
124
|
+
model?: string | null;
|
|
125
|
+
modelTier?: string | null;
|
|
126
|
+
harnessProvider: ProviderName;
|
|
127
|
+
env?: Record<string, string | undefined>;
|
|
128
|
+
}): { model?: string; source: "model" | "modelTier" | "none" } {
|
|
129
|
+
const model = opts.model?.trim();
|
|
130
|
+
if (model) return { model, source: "model" };
|
|
131
|
+
|
|
132
|
+
const tierModel = resolveModelTier({
|
|
133
|
+
tier: opts.modelTier,
|
|
134
|
+
harnessProvider: opts.harnessProvider,
|
|
135
|
+
env: opts.env,
|
|
136
|
+
});
|
|
137
|
+
if (tierModel) return { model: tierModel, source: "modelTier" };
|
|
138
|
+
|
|
139
|
+
return { source: "none" };
|
|
140
|
+
}
|
|
@@ -470,6 +470,8 @@ class ClaudeSession implements ProviderSession {
|
|
|
470
470
|
private sessionMcpConfig: string | null = null,
|
|
471
471
|
private claudeBinaryArgv: readonly string[] = ["claude"],
|
|
472
472
|
systemPromptFile: string | null = null,
|
|
473
|
+
private harnessVariant?: string,
|
|
474
|
+
private harnessVariantMeta?: Record<string, unknown>,
|
|
473
475
|
) {
|
|
474
476
|
this.taskFilePid = taskFilePid;
|
|
475
477
|
this.contextWindowSize = getContextWindowSize(model);
|
|
@@ -682,7 +684,13 @@ class ClaudeSession implements ProviderSession {
|
|
|
682
684
|
// Session ID from init message
|
|
683
685
|
if (json.type === "system" && json.subtype === "init" && json.session_id) {
|
|
684
686
|
this._sessionId = json.session_id;
|
|
685
|
-
this.emit({
|
|
687
|
+
this.emit({
|
|
688
|
+
type: "session_init",
|
|
689
|
+
sessionId: json.session_id,
|
|
690
|
+
provider: "claude",
|
|
691
|
+
...(this.harnessVariant ? { harnessVariant: this.harnessVariant } : {}),
|
|
692
|
+
...(this.harnessVariantMeta ? { harnessVariantMeta: this.harnessVariantMeta } : {}),
|
|
693
|
+
});
|
|
686
694
|
if (json.model) {
|
|
687
695
|
// Phase 4: the CLI's `init.model` reflects the actual model after any
|
|
688
696
|
// backoff/fallback. Update `this.model` so subsequent CostData rows
|
|
@@ -970,6 +978,28 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
970
978
|
}
|
|
971
979
|
}
|
|
972
980
|
|
|
981
|
+
const harnessVariant = useClaudeBridge ? "bridge" : "stock";
|
|
982
|
+
let harnessVariantMeta: Record<string, unknown> | undefined;
|
|
983
|
+
if (useClaudeBridge) {
|
|
984
|
+
try {
|
|
985
|
+
const bin = effectiveClaudeBinaryArgv[0] ?? "claude-bridge";
|
|
986
|
+
const result = await Bun.$`${bin} --version`.quiet();
|
|
987
|
+
const trimmed = result.text().trim();
|
|
988
|
+
if (trimmed) harnessVariantMeta = { version: trimmed };
|
|
989
|
+
} catch {
|
|
990
|
+
// bridge version is best-effort
|
|
991
|
+
}
|
|
992
|
+
} else {
|
|
993
|
+
try {
|
|
994
|
+
const bin = effectiveClaudeBinaryArgv[0] ?? "claude";
|
|
995
|
+
const result = await Bun.$`${bin} --version`.quiet();
|
|
996
|
+
const trimmed = result.text().trim();
|
|
997
|
+
if (trimmed) harnessVariantMeta = { version: trimmed };
|
|
998
|
+
} catch {
|
|
999
|
+
// stock version is best-effort
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
973
1003
|
return new ClaudeSession(
|
|
974
1004
|
config,
|
|
975
1005
|
model,
|
|
@@ -978,6 +1008,8 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
978
1008
|
sessionMcpConfig,
|
|
979
1009
|
effectiveClaudeBinaryArgv,
|
|
980
1010
|
systemPromptFile,
|
|
1011
|
+
harnessVariant,
|
|
1012
|
+
harnessVariantMeta,
|
|
981
1013
|
);
|
|
982
1014
|
}
|
|
983
1015
|
|
|
@@ -69,6 +69,7 @@ import { scrubSecrets } from "../utils/secret-scrubber";
|
|
|
69
69
|
import { computeClaudeManagedCostUsd } from "./claude-managed-models";
|
|
70
70
|
import { getRuntimeFeePerHour } from "./claude-managed-pricing";
|
|
71
71
|
import { createClaudeManagedSwarmEventHandler } from "./claude-managed-swarm-events";
|
|
72
|
+
import { readPkgVersion } from "./harness-version";
|
|
72
73
|
import type {
|
|
73
74
|
CostData,
|
|
74
75
|
CredStatus,
|
|
@@ -639,11 +640,13 @@ class ClaudeManagedSession implements ProviderSession {
|
|
|
639
640
|
// 3. Emit `session_init` once the session is wired up. Listeners
|
|
640
641
|
// attached via `onEvent` will see this either immediately (if they
|
|
641
642
|
// attached pre-emit) or via the queue flush.
|
|
643
|
+
const sdkVersion = readPkgVersion("@anthropic-ai/sdk");
|
|
642
644
|
this.emit({
|
|
643
645
|
type: "session_init",
|
|
644
646
|
sessionId: this._sessionId,
|
|
645
647
|
provider: "claude-managed",
|
|
646
648
|
providerMeta: { managed: true },
|
|
649
|
+
...(sdkVersion ? { harnessVariantMeta: { version: sdkVersion } } : {}),
|
|
647
650
|
});
|
|
648
651
|
|
|
649
652
|
// 4. Drain the SSE stream.
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
|
|
26
26
|
/** Models supported by the managed-agents surface for the swarm worker. */
|
|
27
27
|
export const CLAUDE_MANAGED_MODELS = [
|
|
28
|
+
"claude-fable-5",
|
|
29
|
+
"claude-mythos-5",
|
|
28
30
|
"claude-sonnet-4-6",
|
|
29
31
|
"claude-opus-4-8",
|
|
30
32
|
"claude-opus-4-7",
|
|
@@ -50,6 +52,8 @@ export interface ClaudeManagedModelPricing {
|
|
|
50
52
|
* Anthropic public list pricing. Source:
|
|
51
53
|
* https://platform.claude.com/docs/en/about-claude/pricing
|
|
52
54
|
*
|
|
55
|
+
* - claude-fable-5: $10 / $50 / $1.00 / $12.50 (verified 2026-06-10)
|
|
56
|
+
* - claude-mythos-5: $10 / $50 / $1.00 / $12.50 (limited availability, verified 2026-06-10)
|
|
53
57
|
* - claude-sonnet-4-6: $3 / $15 / $0.30 / $3.75 (in / out / cache-read / cache-write)
|
|
54
58
|
* - claude-opus-4-8: $5 / $25 / $0.50 / $6.25 (verified 2026-05-28)
|
|
55
59
|
* - claude-opus-4-7: $15 / $75 / $1.50 / $18.75 (STALE — was correct at launch, Anthropic has since dropped Opus to $5/$25)
|
|
@@ -57,6 +61,18 @@ export interface ClaudeManagedModelPricing {
|
|
|
57
61
|
* - claude-haiku-4-5: $1 / $5 / $0.10 / $1.25
|
|
58
62
|
*/
|
|
59
63
|
export const CLAUDE_MANAGED_MODEL_PRICING: Record<ClaudeManagedModel, ClaudeManagedModelPricing> = {
|
|
64
|
+
"claude-fable-5": {
|
|
65
|
+
inputPerMillion: 10.0,
|
|
66
|
+
outputPerMillion: 50.0,
|
|
67
|
+
cacheReadPerMillion: 1.0,
|
|
68
|
+
cacheWritePerMillion: 12.5,
|
|
69
|
+
},
|
|
70
|
+
"claude-mythos-5": {
|
|
71
|
+
inputPerMillion: 10.0,
|
|
72
|
+
outputPerMillion: 50.0,
|
|
73
|
+
cacheReadPerMillion: 1.0,
|
|
74
|
+
cacheWritePerMillion: 12.5,
|
|
75
|
+
},
|
|
60
76
|
"claude-sonnet-4-6": {
|
|
61
77
|
inputPerMillion: 3.0,
|
|
62
78
|
outputPerMillion: 15.0,
|
|
@@ -83,6 +83,7 @@ import { getValidCodexOAuth } from "./codex-oauth/storage.js";
|
|
|
83
83
|
import { resolveCodexPrompt } from "./codex-skill-resolver";
|
|
84
84
|
import { createCodexSwarmEventHandler } from "./codex-swarm-events";
|
|
85
85
|
import { CTX_MODE_NUDGE_EVERY } from "./ctx-mode-env";
|
|
86
|
+
import { readPkgVersion } from "./harness-version";
|
|
86
87
|
import { buildOtelTraceparentEnv } from "./otel-env";
|
|
87
88
|
import type {
|
|
88
89
|
CostData,
|
|
@@ -694,7 +695,13 @@ export class CodexSession implements ProviderSession {
|
|
|
694
695
|
switch (event.type) {
|
|
695
696
|
case "thread.started": {
|
|
696
697
|
this._sessionId = event.thread_id;
|
|
697
|
-
|
|
698
|
+
const codexVersion = readPkgVersion("@openai/codex-sdk");
|
|
699
|
+
this.emit({
|
|
700
|
+
type: "session_init",
|
|
701
|
+
sessionId: event.thread_id,
|
|
702
|
+
provider: "codex",
|
|
703
|
+
...(codexVersion ? { harnessVariantMeta: { version: codexVersion } } : {}),
|
|
704
|
+
});
|
|
698
705
|
break;
|
|
699
706
|
}
|
|
700
707
|
case "turn.started": {
|
|
@@ -36,6 +36,7 @@ export const CODEX_DEFAULT_MODEL: CodexModel = "gpt-5.4";
|
|
|
36
36
|
* a task authored for Claude works unchanged when pointed at a Codex worker.
|
|
37
37
|
*/
|
|
38
38
|
const CLAUDE_SHORTNAMES: Record<string, CodexModel> = {
|
|
39
|
+
fable: "gpt-5.5",
|
|
39
40
|
opus: "gpt-5.4",
|
|
40
41
|
sonnet: "gpt-5.4",
|
|
41
42
|
haiku: "gpt-5.4-mini",
|
|
@@ -21,6 +21,7 @@ import { validateOpencodeCredentials } from "../utils/credentials";
|
|
|
21
21
|
import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
|
|
22
22
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
23
23
|
import { CTX_MODE_NUDGE_EVERY } from "./ctx-mode-env";
|
|
24
|
+
import { readPkgVersion } from "./harness-version";
|
|
24
25
|
import type {
|
|
25
26
|
CostData,
|
|
26
27
|
CredCheckOptions,
|
|
@@ -210,7 +211,7 @@ export class OpencodeSession implements ProviderSession {
|
|
|
210
211
|
// The runner attaches its listener after `await adapter.createSession(...)`
|
|
211
212
|
// resolves, but events queued via Promise.resolve().then(...) inside
|
|
212
213
|
// createSession fire on the next microtask — *before* that listener call —
|
|
213
|
-
// so the runner would miss session_init and never PUT /
|
|
214
|
+
// so the runner would miss session_init and never PUT /session,
|
|
214
215
|
// leaving agent_tasks.provider/.model NULL. Buffer + flush on first attach.
|
|
215
216
|
private pendingEvents: ProviderEvent[] = [];
|
|
216
217
|
private completionResolve!: (result: ProviderResult) => void;
|
|
@@ -280,8 +281,14 @@ export class OpencodeSession implements ProviderSession {
|
|
|
280
281
|
|
|
281
282
|
/** Emit the synthetic session_init event. Called by the adapter immediately
|
|
282
283
|
* after construction; buffers if no listener is attached yet. */
|
|
283
|
-
emitSessionInit(provider: "opencode"): void {
|
|
284
|
-
this.emit({
|
|
284
|
+
emitSessionInit(provider: "opencode", harnessVariantMeta?: Record<string, unknown>): void {
|
|
285
|
+
this.emit({
|
|
286
|
+
type: "session_init",
|
|
287
|
+
sessionId: this._sessionId,
|
|
288
|
+
provider,
|
|
289
|
+
harnessVariant: "stock",
|
|
290
|
+
...(harnessVariantMeta ? { harnessVariantMeta } : {}),
|
|
291
|
+
});
|
|
285
292
|
}
|
|
286
293
|
|
|
287
294
|
onEvent(listener: (event: ProviderEvent) => void): void {
|
|
@@ -767,7 +774,8 @@ export class OpencodeAdapter implements ProviderAdapter {
|
|
|
767
774
|
|
|
768
775
|
// Emit session_init synchronously; the session buffers events until the
|
|
769
776
|
// runner's `onEvent(listener)` call attaches a listener.
|
|
770
|
-
|
|
777
|
+
const opcVersion = readPkgVersion("@opencode-ai/sdk");
|
|
778
|
+
session.emitSessionInit("opencode", opcVersion ? { version: opcVersion } : undefined);
|
|
771
779
|
|
|
772
780
|
// Subscribe to SSE events and drive the session
|
|
773
781
|
client.event
|
|
@@ -25,7 +25,9 @@ import {
|
|
|
25
25
|
SessionManager,
|
|
26
26
|
} from "@earendil-works/pi-coding-agent";
|
|
27
27
|
import { type TSchema, Type } from "typebox";
|
|
28
|
+
import { classifyAwsSdkError } from "../utils/aws-error-classifier";
|
|
28
29
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
30
|
+
import { readPkgVersion } from "./harness-version";
|
|
29
31
|
import { createSwarmHooksExtension } from "./pi-mono-extension";
|
|
30
32
|
import { McpHttpClient } from "./pi-mono-mcp-client";
|
|
31
33
|
import type {
|
|
@@ -173,6 +175,7 @@ function mcpToolsToDefinitions(
|
|
|
173
175
|
* (`anthropic/claude-{opus,sonnet,haiku}-*`).
|
|
174
176
|
*/
|
|
175
177
|
const ANTHROPIC_SHORTNAME_OPENROUTER_MIRROR: Record<string, string> = {
|
|
178
|
+
fable: "anthropic/claude-fable-5",
|
|
176
179
|
opus: "anthropic/claude-opus-4",
|
|
177
180
|
sonnet: "anthropic/claude-sonnet-4",
|
|
178
181
|
haiku: "anthropic/claude-haiku-4.5",
|
|
@@ -233,7 +236,8 @@ export function resolveModel(
|
|
|
233
236
|
if (!modelStr) return undefined;
|
|
234
237
|
|
|
235
238
|
const lower = modelStr.toLowerCase();
|
|
236
|
-
const isAnthropicShortname =
|
|
239
|
+
const isAnthropicShortname =
|
|
240
|
+
lower === "opus" || lower === "sonnet" || lower === "haiku" || lower === "fable";
|
|
237
241
|
|
|
238
242
|
// Reroute anthropic shortnames through OpenRouter when no anthropic cred
|
|
239
243
|
// is available. The OpenRouter mirror IDs (`anthropic/claude-sonnet-4`,
|
|
@@ -251,6 +255,7 @@ export function resolveModel(
|
|
|
251
255
|
|
|
252
256
|
// Map common shortnames to provider/model pairs (native anthropic path).
|
|
253
257
|
const shortnames: Record<string, [string, string]> = {
|
|
258
|
+
fable: ["anthropic", "claude-fable-5"],
|
|
254
259
|
opus: ["anthropic", "claude-opus-4-20250514"],
|
|
255
260
|
sonnet: ["anthropic", "claude-sonnet-4-20250514"],
|
|
256
261
|
haiku: ["anthropic", "claude-haiku-4-5-20251001"],
|
|
@@ -357,6 +362,18 @@ export class PiMonoSession implements ProviderSession {
|
|
|
357
362
|
* surface it directly.
|
|
358
363
|
*/
|
|
359
364
|
private prevOutputTokens = 0;
|
|
365
|
+
/**
|
|
366
|
+
* Terminal error message captured from structured pi-coding-agent events.
|
|
367
|
+
*
|
|
368
|
+
* Set by `message_end` (assistant turn with `stopReason==='error'` — covers
|
|
369
|
+
* NON-retryable failures, including AWS auth which never enters pi's retry
|
|
370
|
+
* loop) and by `auto_retry_end` with `success:false` (the definitive terminal
|
|
371
|
+
* failure after the retryable class — throttle / 5xx / timeout — exhausts).
|
|
372
|
+
* Cleared on recovery: a successful `message_end` or an `auto_retry_end` with
|
|
373
|
+
* `success:true` resets it to null, so a recovered error never surfaces as a
|
|
374
|
+
* false failure. Evaluated once at session end in `runSession()`.
|
|
375
|
+
*/
|
|
376
|
+
private terminalError: string | null = null;
|
|
360
377
|
|
|
361
378
|
constructor(agentSession: AgentSession, config: ProviderSessionConfig, createdSymlink: boolean) {
|
|
362
379
|
this.agentSession = agentSession;
|
|
@@ -367,7 +384,14 @@ export class PiMonoSession implements ProviderSession {
|
|
|
367
384
|
this.sessionStartedAt = Date.now();
|
|
368
385
|
|
|
369
386
|
// Emit session_init immediately
|
|
370
|
-
|
|
387
|
+
const piVersion = readPkgVersion("@earendil-works/pi-coding-agent");
|
|
388
|
+
this.emit({
|
|
389
|
+
type: "session_init",
|
|
390
|
+
sessionId: this._sessionId,
|
|
391
|
+
provider: "pi",
|
|
392
|
+
harnessVariant: "stock",
|
|
393
|
+
...(piVersion ? { harnessVariantMeta: { version: piVersion } } : {}),
|
|
394
|
+
});
|
|
371
395
|
|
|
372
396
|
// Subscribe to agent events and normalize
|
|
373
397
|
this.agentSession.subscribe((event) => this.handleAgentEvent(event));
|
|
@@ -414,6 +438,25 @@ export class PiMonoSession implements ProviderSession {
|
|
|
414
438
|
switch (event.type) {
|
|
415
439
|
case "message_end": {
|
|
416
440
|
// Pi emits message_end for user, assistant, and tool-result messages.
|
|
441
|
+
// An assistant turn that ended in `stopReason==='error'` is a failed
|
|
442
|
+
// turn — track it as the (so far) terminal error. This is the ONLY
|
|
443
|
+
// structured signal for NON-retryable failures (AWS auth: ExpiredToken
|
|
444
|
+
// / CredentialsProviderError), which never enter pi's retry loop.
|
|
445
|
+
const endMsg = event.message as {
|
|
446
|
+
role?: string;
|
|
447
|
+
stopReason?: string;
|
|
448
|
+
errorMessage?: string;
|
|
449
|
+
};
|
|
450
|
+
if (endMsg.role === "assistant") {
|
|
451
|
+
if (endMsg.stopReason === "error") {
|
|
452
|
+
// Candidate terminal failure. May still be cleared by a successful
|
|
453
|
+
// retry (auto_retry_end success / a later good message_end).
|
|
454
|
+
this.terminalError = endMsg.errorMessage ?? this.terminalError ?? "Unknown error";
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
// A successful assistant turn means any prior error has recovered.
|
|
458
|
+
this.terminalError = null;
|
|
459
|
+
}
|
|
417
460
|
// Only assistant text should be printed or used as fallback output.
|
|
418
461
|
const text = extractPiAssistantText(event.message);
|
|
419
462
|
if (text) {
|
|
@@ -507,12 +550,18 @@ export class PiMonoSession implements ProviderSession {
|
|
|
507
550
|
result: event.result,
|
|
508
551
|
});
|
|
509
552
|
break;
|
|
510
|
-
case "
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
553
|
+
case "auto_retry_end": {
|
|
554
|
+
// Definitive terminal signal for the RETRYABLE error class
|
|
555
|
+
// (throttle / 5xx / timeout). pi-coding-agent emits success:false with
|
|
556
|
+
// `finalError` only after every retry attempt is exhausted; success:true
|
|
557
|
+
// means the turn recovered, so clear any tracked error.
|
|
558
|
+
if (event.success) {
|
|
559
|
+
this.terminalError = null;
|
|
560
|
+
} else {
|
|
561
|
+
this.terminalError = event.finalError ?? this.terminalError ?? "Unknown error";
|
|
562
|
+
}
|
|
515
563
|
break;
|
|
564
|
+
}
|
|
516
565
|
}
|
|
517
566
|
}
|
|
518
567
|
|
|
@@ -530,6 +579,26 @@ export class PiMonoSession implements ProviderSession {
|
|
|
530
579
|
const stats = this.agentSession.getSessionStats();
|
|
531
580
|
const cost = this.buildCostData(stats);
|
|
532
581
|
|
|
582
|
+
// A structured terminal error from pi-coding-agent events is failure by
|
|
583
|
+
// definition (the agent already exhausted retries or hit a non-retryable
|
|
584
|
+
// error). Surface it so the session-chat red box fires and the task fails,
|
|
585
|
+
// exactly like sibling adapters. AWS errors get a categorized, actionable
|
|
586
|
+
// message; anything else surfaces its raw error text.
|
|
587
|
+
if (this.terminalError) {
|
|
588
|
+
const classification = classifyAwsSdkError(this.terminalError);
|
|
589
|
+
const message = classification?.message ?? this.terminalError;
|
|
590
|
+
const category = classification?.category;
|
|
591
|
+
this.emit({ type: "error", message, category });
|
|
592
|
+
return {
|
|
593
|
+
exitCode: 1,
|
|
594
|
+
sessionId: this._sessionId,
|
|
595
|
+
cost,
|
|
596
|
+
isError: true,
|
|
597
|
+
errorCategory: category,
|
|
598
|
+
failureReason: message,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
533
602
|
this.emit({
|
|
534
603
|
type: "result",
|
|
535
604
|
cost,
|
|
@@ -545,13 +614,26 @@ export class PiMonoSession implements ProviderSession {
|
|
|
545
614
|
};
|
|
546
615
|
} catch (err) {
|
|
547
616
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
617
|
+
// Defense-in-depth: AWS SDK failures surface as structured events (handled
|
|
618
|
+
// above in runSession), not thrown exceptions, so this catch is for genuine
|
|
619
|
+
// unexpected throws (MCP / transport / etc). Still classify in case an AWS
|
|
620
|
+
// signature ever reaches here, so the red box fires like sibling adapters.
|
|
621
|
+
const awsCatchError = classifyAwsSdkError(errorMessage);
|
|
622
|
+
if (awsCatchError) {
|
|
623
|
+
this.emit({
|
|
624
|
+
type: "error",
|
|
625
|
+
message: awsCatchError.message,
|
|
626
|
+
category: awsCatchError.category,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
548
629
|
this.emit({ type: "raw_stderr", content: `[pi-mono] Error: ${errorMessage}\n` });
|
|
549
630
|
|
|
550
631
|
return {
|
|
551
632
|
exitCode: 1,
|
|
552
633
|
sessionId: this._sessionId,
|
|
553
634
|
isError: true,
|
|
554
|
-
|
|
635
|
+
errorCategory: awsCatchError?.category,
|
|
636
|
+
failureReason: awsCatchError?.message ?? errorMessage,
|
|
555
637
|
};
|
|
556
638
|
} finally {
|
|
557
639
|
await this.logFileHandle.end();
|
package/src/providers/types.ts
CHANGED
|
@@ -42,6 +42,8 @@ export type ProviderEvent =
|
|
|
42
42
|
sessionId: string;
|
|
43
43
|
provider?: ProviderName;
|
|
44
44
|
providerMeta?: Record<string, unknown>;
|
|
45
|
+
harnessVariant?: string;
|
|
46
|
+
harnessVariantMeta?: Record<string, unknown>;
|
|
45
47
|
}
|
|
46
48
|
| { type: "message"; role: "assistant" | "user"; content: string }
|
|
47
49
|
| { type: "tool_start"; toolCallId: string; toolName: string; args: unknown }
|
|
@@ -3,7 +3,7 @@ import { CronExpressionParser } from "cron-parser";
|
|
|
3
3
|
import { getDb, getDueScheduledTasks, getScheduledTaskById, updateScheduledTask } from "@/be/db";
|
|
4
4
|
import { scheduleContextKey } from "@/tasks/context-key";
|
|
5
5
|
import { createTaskWithSiblingAwareness } from "@/tasks/sibling-awareness";
|
|
6
|
-
import type { ScheduledTask } from "@/types";
|
|
6
|
+
import type { AgentTask, ScheduledTask } from "@/types";
|
|
7
7
|
import type { ExecutorRegistry } from "@/workflows/executors/registry";
|
|
8
8
|
import { handleScheduleTrigger } from "@/workflows/triggers";
|
|
9
9
|
|
|
@@ -11,6 +11,24 @@ let schedulerInterval: ReturnType<typeof setInterval> | null = null;
|
|
|
11
11
|
let isProcessing = false;
|
|
12
12
|
let executorRegistry: ExecutorRegistry | null = null;
|
|
13
13
|
|
|
14
|
+
export function createStandaloneScheduleTask(
|
|
15
|
+
schedule: ScheduledTask,
|
|
16
|
+
extraTags: string[] = [],
|
|
17
|
+
): AgentTask {
|
|
18
|
+
return createTaskWithSiblingAwareness(schedule.taskTemplate, {
|
|
19
|
+
creatorAgentId: schedule.createdByAgentId,
|
|
20
|
+
taskType: schedule.taskType,
|
|
21
|
+
tags: [...schedule.tags, "scheduled", `schedule:${schedule.name}`, ...extraTags],
|
|
22
|
+
priority: schedule.priority,
|
|
23
|
+
agentId: schedule.targetAgentId,
|
|
24
|
+
model: schedule.model,
|
|
25
|
+
modelTier: schedule.modelTier,
|
|
26
|
+
scheduleId: schedule.id,
|
|
27
|
+
source: "schedule",
|
|
28
|
+
contextKey: scheduleContextKey({ scheduleId: schedule.id }),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
/**
|
|
15
33
|
* Recover missed scheduled task runs from downtime.
|
|
16
34
|
* Fires ONE catch-up run per schedule (not N missed runs).
|
|
@@ -45,17 +63,7 @@ async function recoverMissedSchedules(): Promise<void> {
|
|
|
45
63
|
|
|
46
64
|
if (!triggeredWorkflows) {
|
|
47
65
|
const tx = getDb().transaction(() => {
|
|
48
|
-
|
|
49
|
-
creatorAgentId: schedule.createdByAgentId,
|
|
50
|
-
taskType: schedule.taskType,
|
|
51
|
-
tags: [...schedule.tags, "scheduled", `schedule:${schedule.name}`, "recovered"],
|
|
52
|
-
priority: schedule.priority,
|
|
53
|
-
agentId: schedule.targetAgentId,
|
|
54
|
-
model: schedule.model,
|
|
55
|
-
scheduleId: schedule.id,
|
|
56
|
-
source: "schedule",
|
|
57
|
-
contextKey: scheduleContextKey({ scheduleId: schedule.id }),
|
|
58
|
-
});
|
|
66
|
+
createStandaloneScheduleTask(schedule, ["recovered"]);
|
|
59
67
|
});
|
|
60
68
|
tx();
|
|
61
69
|
}
|
|
@@ -150,17 +158,7 @@ async function executeSchedule(schedule: ScheduledTask): Promise<void> {
|
|
|
150
158
|
if (!triggeredWorkflows) {
|
|
151
159
|
// No workflows linked — create standalone task (existing behavior)
|
|
152
160
|
getDb().transaction(() => {
|
|
153
|
-
|
|
154
|
-
creatorAgentId: schedule.createdByAgentId,
|
|
155
|
-
taskType: schedule.taskType,
|
|
156
|
-
tags: [...schedule.tags, "scheduled", `schedule:${schedule.name}`],
|
|
157
|
-
priority: schedule.priority,
|
|
158
|
-
agentId: schedule.targetAgentId,
|
|
159
|
-
model: schedule.model,
|
|
160
|
-
scheduleId: schedule.id,
|
|
161
|
-
source: "schedule",
|
|
162
|
-
contextKey: scheduleContextKey({ scheduleId: schedule.id }),
|
|
163
|
-
});
|
|
161
|
+
createStandaloneScheduleTask(schedule);
|
|
164
162
|
})();
|
|
165
163
|
}
|
|
166
164
|
|
|
@@ -341,17 +339,7 @@ export async function runScheduleNow(scheduleId: string): Promise<void> {
|
|
|
341
339
|
if (!triggeredWorkflows) {
|
|
342
340
|
// No workflows linked — create standalone task (existing behavior)
|
|
343
341
|
getDb().transaction(() => {
|
|
344
|
-
|
|
345
|
-
creatorAgentId: schedule.createdByAgentId,
|
|
346
|
-
taskType: schedule.taskType,
|
|
347
|
-
tags: [...schedule.tags, "scheduled", `schedule:${schedule.name}`, "manual-run"],
|
|
348
|
-
priority: schedule.priority,
|
|
349
|
-
agentId: schedule.targetAgentId,
|
|
350
|
-
model: schedule.model,
|
|
351
|
-
scheduleId: schedule.id,
|
|
352
|
-
source: "schedule",
|
|
353
|
-
contextKey: scheduleContextKey({ scheduleId: schedule.id }),
|
|
354
|
-
});
|
|
342
|
+
createStandaloneScheduleTask(schedule, ["manual-run"]);
|
|
355
343
|
})();
|
|
356
344
|
}
|
|
357
345
|
|