@desplega.ai/agent-swarm 1.93.0 → 1.95.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 +4 -3
- package/src/be/db.ts +74 -9
- 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/094_mcp_extra_authorize_params.sql +4 -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/be/skill-sync.ts +4 -4
- package/src/be/swarm-config-guard.ts +8 -0
- package/src/commands/provider-credentials.ts +14 -8
- package/src/commands/runner.ts +84 -13
- package/src/http/index.ts +13 -2
- package/src/http/mcp-oauth.ts +14 -0
- 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/oauth/mcp-wrapper.ts +14 -0
- package/src/providers/claude-managed-models.ts +9 -0
- package/src/providers/codex-skill-resolver.ts +22 -8
- package/src/providers/opencode-adapter.ts +21 -2
- package/src/providers/pi-mono-adapter.ts +143 -26
- package/src/providers/types.ts +12 -0
- 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/credential-check.test.ts +185 -46
- package/src/tests/harness-provider-resolution.test.ts +23 -0
- package/src/tests/http-api-integration.test.ts +19 -0
- package/src/tests/mcp-oauth-queries.test.ts +71 -1
- package/src/tests/mcp-oauth-wrapper.test.ts +109 -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 +38 -1
- package/src/tests/pi-mono-adapter.test.ts +319 -0
- package/src/tests/provider-command-format.test.ts +12 -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/skill-fs-writer.test.ts +7 -1
- package/src/tests/skill-sync.test.ts +15 -3
- 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/mcp-servers/mcp-server-create.ts +7 -0
- package/src/tools/mcp-servers/mcp-server-update.ts +8 -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 +30 -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/utils/skill-fs-writer.ts +11 -3
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/executors/agent-task.ts +3 -1
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { validateOpencodeCredentials } from "../utils/credentials";
|
|
21
21
|
import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
|
|
22
22
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
23
|
+
import { resolveSlashSkillPrompt } from "./codex-skill-resolver";
|
|
23
24
|
import { CTX_MODE_NUDGE_EVERY } from "./ctx-mode-env";
|
|
24
25
|
import { readPkgVersion } from "./harness-version";
|
|
25
26
|
import type {
|
|
@@ -102,6 +103,13 @@ function isAssistantMessage(msg: unknown): msg is AssistantMessage {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
const DOCKER_PLUGIN_PATH = "/home/worker/.config/opencode/plugins/agent-swarm.ts";
|
|
106
|
+
|
|
107
|
+
function defaultOpencodeSkillsDir(): string {
|
|
108
|
+
if (process.env.OPENCODE_SKILLS_DIR) {
|
|
109
|
+
return process.env.OPENCODE_SKILLS_DIR;
|
|
110
|
+
}
|
|
111
|
+
return join(process.env.HOME ?? "/home/worker", ".opencode", "skills");
|
|
112
|
+
}
|
|
105
113
|
const MODEL_CACHE_REFRESH_TIMEOUT_MS = 15_000;
|
|
106
114
|
|
|
107
115
|
function isOpenRouterModel(model: string | undefined): boolean {
|
|
@@ -286,10 +294,15 @@ export class OpencodeSession implements ProviderSession {
|
|
|
286
294
|
type: "session_init",
|
|
287
295
|
sessionId: this._sessionId,
|
|
288
296
|
provider,
|
|
297
|
+
harnessVariant: "stock",
|
|
289
298
|
...(harnessVariantMeta ? { harnessVariantMeta } : {}),
|
|
290
299
|
});
|
|
291
300
|
}
|
|
292
301
|
|
|
302
|
+
emitProviderEvent(event: ProviderEvent): void {
|
|
303
|
+
this.emit(event);
|
|
304
|
+
}
|
|
305
|
+
|
|
293
306
|
onEvent(listener: (event: ProviderEvent) => void): void {
|
|
294
307
|
const wasEmpty = this.listeners.length === 0;
|
|
295
308
|
this.listeners.push(listener);
|
|
@@ -737,13 +750,19 @@ export class OpencodeAdapter implements ProviderAdapter {
|
|
|
737
750
|
|
|
738
751
|
let promptRefreshAttempted = false;
|
|
739
752
|
let promptRefreshPromise: Promise<boolean> | undefined;
|
|
753
|
+
let session: OpencodeSession | undefined;
|
|
740
754
|
const sendPrompt = async () => {
|
|
755
|
+
const resolvedPrompt = await resolveSlashSkillPrompt(config.prompt, {
|
|
756
|
+
providerLabel: "opencode",
|
|
757
|
+
skillsDir: defaultOpencodeSkillsDir(),
|
|
758
|
+
emit: (event) => session?.emitProviderEvent(event),
|
|
759
|
+
});
|
|
741
760
|
await client.session.prompt({
|
|
742
761
|
path: { id: sessionId },
|
|
743
762
|
query: { directory: config.cwd },
|
|
744
763
|
body: {
|
|
745
764
|
agent: agentName,
|
|
746
|
-
parts: [{ type: "text", text:
|
|
765
|
+
parts: [{ type: "text", text: resolvedPrompt }],
|
|
747
766
|
},
|
|
748
767
|
});
|
|
749
768
|
};
|
|
@@ -759,7 +778,7 @@ export class OpencodeAdapter implements ProviderAdapter {
|
|
|
759
778
|
return await promptRefreshPromise;
|
|
760
779
|
};
|
|
761
780
|
|
|
762
|
-
|
|
781
|
+
session = new OpencodeSession(
|
|
763
782
|
sessionId,
|
|
764
783
|
server,
|
|
765
784
|
config.model,
|
|
@@ -25,6 +25,7 @@ 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";
|
|
29
30
|
import { readPkgVersion } from "./harness-version";
|
|
30
31
|
import { createSwarmHooksExtension } from "./pi-mono-extension";
|
|
@@ -73,40 +74,85 @@ function modelToCredKeys(modelStr: string | undefined): string[] | null {
|
|
|
73
74
|
return null;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Run a single `ListFoundationModels` call against the AWS Bedrock management
|
|
79
|
+
* API to verify that the active credential chain is valid for Bedrock in the
|
|
80
|
+
* configured region. Returns the client directly (callers discard the model
|
|
81
|
+
* list — only the throw/no-throw distinction is the signal).
|
|
82
|
+
*
|
|
83
|
+
* Dynamically imported so the API binary never loads `@aws-sdk/client-bedrock`.
|
|
84
|
+
* Tests inject a stub via `CredCheckOptions.bedrockProbe` instead.
|
|
85
|
+
*/
|
|
86
|
+
async function runBedrockSdkProbe(region: string): Promise<void> {
|
|
87
|
+
const { BedrockClient, ListFoundationModelsCommand } = await import("@aws-sdk/client-bedrock");
|
|
88
|
+
const client = new BedrockClient({ region });
|
|
89
|
+
await client.send(new ListFoundationModelsCommand({}));
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
/**
|
|
77
93
|
* Pi-mono is satisfied by ANY of:
|
|
78
|
-
* 1. `
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
94
|
+
* 1. `BEDROCK_AUTH_MODE=sdk` — or `MODEL_OVERRIDE` selects the
|
|
95
|
+
* `amazon-bedrock` provider (prefix-inference fallback when
|
|
96
|
+
* `BEDROCK_AUTH_MODE` is absent). A real `ListFoundationModels` probe
|
|
97
|
+
* is issued via the AWS SDK default credential chain. Success →
|
|
98
|
+
* `ready:true, satisfiedBy:"sdk-delegated"`; failure → `ready:false`
|
|
99
|
+
* with a classified hint. The probe is worker-only (the pi dynamic-import
|
|
100
|
+
* arm in `checkProviderCredentials`); the API binary never imports the SDK.
|
|
82
101
|
* 2. `~/.pi/agent/auth.json` exists.
|
|
83
|
-
* 3. `MODEL_OVERRIDE` is set to a provider-prefixed model — only
|
|
84
|
-
* matching provider's key is required.
|
|
102
|
+
* 3. `MODEL_OVERRIDE` is set to a non-Bedrock provider-prefixed model — only
|
|
103
|
+
* the matching provider's key is required.
|
|
85
104
|
* 4. `MODEL_OVERRIDE` is empty / unprefixed — any one of the supported
|
|
86
105
|
* keys (ANTHROPIC_API_KEY / OPENROUTER_API_KEY / OPENAI_API_KEY) is
|
|
87
106
|
* enough.
|
|
88
107
|
*
|
|
89
|
-
* Bedrock is checked first so a stale `auth.json` (Anthropic /
|
|
90
|
-
* creds from a previous login) doesn't get falsely reported as
|
|
91
|
-
* satisfying source when the model is actually going to AWS.
|
|
108
|
+
* The Bedrock branch is checked first so a stale `auth.json` (Anthropic /
|
|
109
|
+
* OpenRouter creds from a previous login) doesn't get falsely reported as
|
|
110
|
+
* the satisfying source when the model is actually going to AWS.
|
|
92
111
|
*/
|
|
93
|
-
export function checkPiMonoCredentials(
|
|
112
|
+
export async function checkPiMonoCredentials(
|
|
94
113
|
env: Record<string, string | undefined>,
|
|
95
114
|
opts: CredCheckOptions = {},
|
|
96
|
-
): CredStatus {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
): Promise<CredStatus> {
|
|
116
|
+
// Determine Bedrock SDK mode:
|
|
117
|
+
// - Explicit: BEDROCK_AUTH_MODE=sdk
|
|
118
|
+
// - Fallback: BEDROCK_AUTH_MODE absent AND MODEL_OVERRIDE starts with
|
|
119
|
+
// "amazon-bedrock/" (preserves today's prefix-inference semantics)
|
|
120
|
+
// BEDROCK_AUTH_MODE=bearer is declared/validated but the full bearer-token
|
|
121
|
+
// path is out of scope for PR1 — it falls through to the standard auth check.
|
|
122
|
+
const bedrockAuthMode = env.BEDROCK_AUTH_MODE?.toLowerCase();
|
|
123
|
+
const isBedrockSdk =
|
|
124
|
+
bedrockAuthMode === "sdk" ||
|
|
125
|
+
(bedrockAuthMode === undefined &&
|
|
126
|
+
env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/"));
|
|
127
|
+
|
|
128
|
+
if (isBedrockSdk) {
|
|
129
|
+
const region = env.AWS_REGION ?? "us-east-1";
|
|
130
|
+
const probe = opts.bedrockProbe ?? (() => runBedrockSdkProbe(region));
|
|
131
|
+
try {
|
|
132
|
+
await probe();
|
|
133
|
+
return {
|
|
134
|
+
ready: true,
|
|
135
|
+
missing: [],
|
|
136
|
+
satisfiedBy: "sdk-delegated",
|
|
137
|
+
hint: `AWS SDK credentials verified via ListFoundationModels (region: ${region}).`,
|
|
138
|
+
};
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
141
|
+
const classification = classifyAwsSdkError(errorMessage);
|
|
142
|
+
return {
|
|
143
|
+
ready: false,
|
|
144
|
+
missing: [],
|
|
145
|
+
hint:
|
|
146
|
+
classification?.message ??
|
|
147
|
+
`AWS Bedrock credential probe failed (region: ${region}): ${errorMessage}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
104
150
|
}
|
|
105
151
|
|
|
106
152
|
const homeDir = opts.homeDir ?? env.HOME ?? "/root";
|
|
107
|
-
const
|
|
153
|
+
const fsProbe = opts.fs?.existsSync ?? existsSync;
|
|
108
154
|
const authFile = `${homeDir}/.pi/agent/auth.json`;
|
|
109
|
-
if (
|
|
155
|
+
if (fsProbe(authFile)) {
|
|
110
156
|
return { ready: true, missing: [], satisfiedBy: "file" };
|
|
111
157
|
}
|
|
112
158
|
|
|
@@ -361,6 +407,18 @@ export class PiMonoSession implements ProviderSession {
|
|
|
361
407
|
* surface it directly.
|
|
362
408
|
*/
|
|
363
409
|
private prevOutputTokens = 0;
|
|
410
|
+
/**
|
|
411
|
+
* Terminal error message captured from structured pi-coding-agent events.
|
|
412
|
+
*
|
|
413
|
+
* Set by `message_end` (assistant turn with `stopReason==='error'` — covers
|
|
414
|
+
* NON-retryable failures, including AWS auth which never enters pi's retry
|
|
415
|
+
* loop) and by `auto_retry_end` with `success:false` (the definitive terminal
|
|
416
|
+
* failure after the retryable class — throttle / 5xx / timeout — exhausts).
|
|
417
|
+
* Cleared on recovery: a successful `message_end` or an `auto_retry_end` with
|
|
418
|
+
* `success:true` resets it to null, so a recovered error never surfaces as a
|
|
419
|
+
* false failure. Evaluated once at session end in `runSession()`.
|
|
420
|
+
*/
|
|
421
|
+
private terminalError: string | null = null;
|
|
364
422
|
|
|
365
423
|
constructor(agentSession: AgentSession, config: ProviderSessionConfig, createdSymlink: boolean) {
|
|
366
424
|
this.agentSession = agentSession;
|
|
@@ -376,6 +434,7 @@ export class PiMonoSession implements ProviderSession {
|
|
|
376
434
|
type: "session_init",
|
|
377
435
|
sessionId: this._sessionId,
|
|
378
436
|
provider: "pi",
|
|
437
|
+
harnessVariant: "stock",
|
|
379
438
|
...(piVersion ? { harnessVariantMeta: { version: piVersion } } : {}),
|
|
380
439
|
});
|
|
381
440
|
|
|
@@ -424,6 +483,25 @@ export class PiMonoSession implements ProviderSession {
|
|
|
424
483
|
switch (event.type) {
|
|
425
484
|
case "message_end": {
|
|
426
485
|
// Pi emits message_end for user, assistant, and tool-result messages.
|
|
486
|
+
// An assistant turn that ended in `stopReason==='error'` is a failed
|
|
487
|
+
// turn — track it as the (so far) terminal error. This is the ONLY
|
|
488
|
+
// structured signal for NON-retryable failures (AWS auth: ExpiredToken
|
|
489
|
+
// / CredentialsProviderError), which never enter pi's retry loop.
|
|
490
|
+
const endMsg = event.message as {
|
|
491
|
+
role?: string;
|
|
492
|
+
stopReason?: string;
|
|
493
|
+
errorMessage?: string;
|
|
494
|
+
};
|
|
495
|
+
if (endMsg.role === "assistant") {
|
|
496
|
+
if (endMsg.stopReason === "error") {
|
|
497
|
+
// Candidate terminal failure. May still be cleared by a successful
|
|
498
|
+
// retry (auto_retry_end success / a later good message_end).
|
|
499
|
+
this.terminalError = endMsg.errorMessage ?? this.terminalError ?? "Unknown error";
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
// A successful assistant turn means any prior error has recovered.
|
|
503
|
+
this.terminalError = null;
|
|
504
|
+
}
|
|
427
505
|
// Only assistant text should be printed or used as fallback output.
|
|
428
506
|
const text = extractPiAssistantText(event.message);
|
|
429
507
|
if (text) {
|
|
@@ -517,12 +595,18 @@ export class PiMonoSession implements ProviderSession {
|
|
|
517
595
|
result: event.result,
|
|
518
596
|
});
|
|
519
597
|
break;
|
|
520
|
-
case "
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
598
|
+
case "auto_retry_end": {
|
|
599
|
+
// Definitive terminal signal for the RETRYABLE error class
|
|
600
|
+
// (throttle / 5xx / timeout). pi-coding-agent emits success:false with
|
|
601
|
+
// `finalError` only after every retry attempt is exhausted; success:true
|
|
602
|
+
// means the turn recovered, so clear any tracked error.
|
|
603
|
+
if (event.success) {
|
|
604
|
+
this.terminalError = null;
|
|
605
|
+
} else {
|
|
606
|
+
this.terminalError = event.finalError ?? this.terminalError ?? "Unknown error";
|
|
607
|
+
}
|
|
525
608
|
break;
|
|
609
|
+
}
|
|
526
610
|
}
|
|
527
611
|
}
|
|
528
612
|
|
|
@@ -540,6 +624,26 @@ export class PiMonoSession implements ProviderSession {
|
|
|
540
624
|
const stats = this.agentSession.getSessionStats();
|
|
541
625
|
const cost = this.buildCostData(stats);
|
|
542
626
|
|
|
627
|
+
// A structured terminal error from pi-coding-agent events is failure by
|
|
628
|
+
// definition (the agent already exhausted retries or hit a non-retryable
|
|
629
|
+
// error). Surface it so the session-chat red box fires and the task fails,
|
|
630
|
+
// exactly like sibling adapters. AWS errors get a categorized, actionable
|
|
631
|
+
// message; anything else surfaces its raw error text.
|
|
632
|
+
if (this.terminalError) {
|
|
633
|
+
const classification = classifyAwsSdkError(this.terminalError);
|
|
634
|
+
const message = classification?.message ?? this.terminalError;
|
|
635
|
+
const category = classification?.category;
|
|
636
|
+
this.emit({ type: "error", message, category });
|
|
637
|
+
return {
|
|
638
|
+
exitCode: 1,
|
|
639
|
+
sessionId: this._sessionId,
|
|
640
|
+
cost,
|
|
641
|
+
isError: true,
|
|
642
|
+
errorCategory: category,
|
|
643
|
+
failureReason: message,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
543
647
|
this.emit({
|
|
544
648
|
type: "result",
|
|
545
649
|
cost,
|
|
@@ -555,13 +659,26 @@ export class PiMonoSession implements ProviderSession {
|
|
|
555
659
|
};
|
|
556
660
|
} catch (err) {
|
|
557
661
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
662
|
+
// Defense-in-depth: AWS SDK failures surface as structured events (handled
|
|
663
|
+
// above in runSession), not thrown exceptions, so this catch is for genuine
|
|
664
|
+
// unexpected throws (MCP / transport / etc). Still classify in case an AWS
|
|
665
|
+
// signature ever reaches here, so the red box fires like sibling adapters.
|
|
666
|
+
const awsCatchError = classifyAwsSdkError(errorMessage);
|
|
667
|
+
if (awsCatchError) {
|
|
668
|
+
this.emit({
|
|
669
|
+
type: "error",
|
|
670
|
+
message: awsCatchError.message,
|
|
671
|
+
category: awsCatchError.category,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
558
674
|
this.emit({ type: "raw_stderr", content: `[pi-mono] Error: ${errorMessage}\n` });
|
|
559
675
|
|
|
560
676
|
return {
|
|
561
677
|
exitCode: 1,
|
|
562
678
|
sessionId: this._sessionId,
|
|
563
679
|
isError: true,
|
|
564
|
-
|
|
680
|
+
errorCategory: awsCatchError?.category,
|
|
681
|
+
failureReason: awsCatchError?.message ?? errorMessage,
|
|
565
682
|
};
|
|
566
683
|
} finally {
|
|
567
684
|
await this.logFileHandle.end();
|
package/src/providers/types.ts
CHANGED
|
@@ -188,8 +188,20 @@ export interface CredStatus {
|
|
|
188
188
|
* pi/opencode predicates probe the filesystem for `~/.codex/auth.json`,
|
|
189
189
|
* `~/.pi/agent/auth.json`, `~/.local/share/opencode/auth.json`. Tests inject
|
|
190
190
|
* a fake `fs` + `homeDir` to exercise the file-vs-env branches deterministically.
|
|
191
|
+
*
|
|
192
|
+
* `bedrockProbe` is an injectable for the Bedrock SDK probe path in
|
|
193
|
+
* `checkPiMonoCredentials`. In production it is left undefined and the
|
|
194
|
+
* function dynamically imports `@aws-sdk/client-bedrock` to run a real
|
|
195
|
+
* `ListFoundationModels` call. Tests inject a stub to avoid hitting AWS.
|
|
191
196
|
*/
|
|
192
197
|
export interface CredCheckOptions {
|
|
193
198
|
homeDir?: string;
|
|
194
199
|
fs?: { existsSync(p: string): boolean };
|
|
200
|
+
/**
|
|
201
|
+
* Injectable for Bedrock SDK credential probe. When provided, called instead
|
|
202
|
+
* of the real `@aws-sdk/client-bedrock` `ListFoundationModels` call.
|
|
203
|
+
* Should throw on auth/access failure (with an AWS SDK-shaped error message)
|
|
204
|
+
* or resolve on success.
|
|
205
|
+
*/
|
|
206
|
+
bedrockProbe?: () => Promise<void>;
|
|
195
207
|
}
|
|
@@ -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
|
|
package/src/server-user.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import pkg from "../package.json";
|
|
4
|
+
import { ModelTierSchema } from "./model-tiers";
|
|
4
5
|
import {
|
|
5
6
|
cancelTaskHandler,
|
|
6
7
|
cancelTaskInputSchema,
|
|
@@ -28,9 +29,14 @@ const userSendTaskInputSchema = z.object({
|
|
|
28
29
|
tags: z.array(z.string()).optional().describe("Tags for filtering (e.g., ['urgent'])."),
|
|
29
30
|
priority: z.number().int().min(0).max(100).optional().describe("Priority 0-100 (default: 50)."),
|
|
30
31
|
model: z
|
|
31
|
-
.
|
|
32
|
+
.string()
|
|
33
|
+
.trim()
|
|
34
|
+
.min(1)
|
|
32
35
|
.optional()
|
|
33
|
-
.describe("
|
|
36
|
+
.describe("Concrete model override interpreted by the assignee's harness/provider."),
|
|
37
|
+
modelTier: ModelTierSchema.optional().describe(
|
|
38
|
+
"Portable model tier: 'smol', 'regular', 'smart', or 'ultra'. Resolved by the assignee's harness/provider.",
|
|
39
|
+
),
|
|
34
40
|
});
|
|
35
41
|
|
|
36
42
|
export function createUserServer(user: User): McpServer {
|
package/src/slack/responses.ts
CHANGED
|
@@ -15,6 +15,20 @@ import {
|
|
|
15
15
|
// Re-export for backward compatibility
|
|
16
16
|
export { markdownToSlack } from "./blocks";
|
|
17
17
|
|
|
18
|
+
export type SlackUpdateResult = "ok" | "not_found" | "failed";
|
|
19
|
+
|
|
20
|
+
function classifySlackUpdateError(error: unknown): SlackUpdateResult {
|
|
21
|
+
const errorCode = (error as { data?: { error?: string } } | undefined)?.data?.error;
|
|
22
|
+
if (
|
|
23
|
+
errorCode === "message_not_found" ||
|
|
24
|
+
errorCode === "channel_not_found" ||
|
|
25
|
+
errorCode === "thread_not_found"
|
|
26
|
+
) {
|
|
27
|
+
return "not_found";
|
|
28
|
+
}
|
|
29
|
+
return "failed";
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
const isDev = process.env.ENV === "development";
|
|
19
33
|
|
|
20
34
|
/**
|
|
@@ -140,12 +154,12 @@ export async function updateProgressInPlace(
|
|
|
140
154
|
task: AgentTask,
|
|
141
155
|
progress: string,
|
|
142
156
|
messageTs: string,
|
|
143
|
-
): Promise<
|
|
157
|
+
): Promise<SlackUpdateResult> {
|
|
144
158
|
const app = getSlackApp();
|
|
145
|
-
if (!app || !task.slackChannelId || !task.agentId) return
|
|
159
|
+
if (!app || !task.slackChannelId || !task.agentId) return "failed";
|
|
146
160
|
|
|
147
161
|
const agent = getAgentById(task.agentId);
|
|
148
|
-
if (!agent) return
|
|
162
|
+
if (!agent) return "failed";
|
|
149
163
|
|
|
150
164
|
const blocks = buildProgressBlocks({ agentName: agent.name, taskId: task.id, progress });
|
|
151
165
|
|
|
@@ -157,10 +171,17 @@ export async function updateProgressInPlace(
|
|
|
157
171
|
// biome-ignore lint/suspicious/noExplicitAny: Block Kit objects
|
|
158
172
|
blocks: blocks as any,
|
|
159
173
|
});
|
|
160
|
-
return
|
|
174
|
+
return "ok";
|
|
161
175
|
} catch (error) {
|
|
162
|
-
|
|
163
|
-
|
|
176
|
+
const result = classifySlackUpdateError(error);
|
|
177
|
+
if (result === "not_found") {
|
|
178
|
+
console.warn(
|
|
179
|
+
`[Slack] Progress message missing for task ${task.id} ts=${messageTs}; will repost`,
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
console.error(`[Slack] Failed to update progress in-place:`, error);
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
164
185
|
}
|
|
165
186
|
}
|
|
166
187
|
|
|
@@ -233,9 +254,9 @@ export async function updateTreeMessage(
|
|
|
233
254
|
messageTs: string,
|
|
234
255
|
blocks: unknown[],
|
|
235
256
|
fallbackText: string,
|
|
236
|
-
): Promise<
|
|
257
|
+
): Promise<SlackUpdateResult> {
|
|
237
258
|
const app = getSlackApp();
|
|
238
|
-
if (!app) return
|
|
259
|
+
if (!app) return "failed";
|
|
239
260
|
|
|
240
261
|
try {
|
|
241
262
|
await app.client.chat.update({
|
|
@@ -245,10 +266,17 @@ export async function updateTreeMessage(
|
|
|
245
266
|
// biome-ignore lint/suspicious/noExplicitAny: Block Kit objects
|
|
246
267
|
blocks: blocks as any,
|
|
247
268
|
});
|
|
248
|
-
return
|
|
269
|
+
return "ok";
|
|
249
270
|
} catch (error) {
|
|
250
|
-
|
|
251
|
-
|
|
271
|
+
const result = classifySlackUpdateError(error);
|
|
272
|
+
if (result === "not_found") {
|
|
273
|
+
console.warn(
|
|
274
|
+
`[Slack] Tree message missing for channel=${channelId} ts=${messageTs}; will repost`,
|
|
275
|
+
);
|
|
276
|
+
} else {
|
|
277
|
+
console.error(`[Slack] Failed to update tree message:`, error);
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
252
280
|
}
|
|
253
281
|
}
|
|
254
282
|
|