@vellumai/assistant 0.9.0 → 0.9.1-staging.1
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/ARCHITECTURE.md +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +4 -4
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +135 -59
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
- package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/config-loader-backfill.test.ts +103 -6
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +3 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -13
- package/src/__tests__/dynamic-page-surface.test.ts +101 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/invite-redemption-service.test.ts +4 -4
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +0 -22
- package/src/__tests__/llm-resolver.test.ts +49 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/persona-resolver.test.ts +11 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/skill-execute-input.test.ts +46 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/subagent-tools.test.ts +116 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +6 -6
- package/src/__tests__/voice-invite-redemption.test.ts +2 -2
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/agent/loop.ts +25 -5
- package/src/api/README.md +6 -6
- package/src/api/responses/conversation-message.ts +2 -4
- package/src/api/responses/home.ts +0 -4
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/plugins.ts +143 -2
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +228 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/upgrade-plugin.ts +270 -10
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +0 -1
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +12 -2
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/loader.ts +4 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/seed-inference-profiles.ts +25 -20
- package/src/contacts/contact-store.ts +87 -96
- package/src/contacts/contacts-write.ts +5 -21
- package/src/context/compactor.ts +2 -2
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop.ts +15 -4
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-tool-setup.ts +11 -3
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/handlers/config-channels.ts +20 -16
- package/src/daemon/handlers/config-slack-channel.ts +2 -3
- package/src/daemon/lifecycle.ts +0 -7
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/home/relationship-state.ts +2 -4
- package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/db-init.ts +7 -17
- package/src/memory/embedding-backend.ts +23 -0
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +52 -0
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-job.ts +19 -227
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/schema/contacts.ts +0 -4
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +5 -32
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/broadcaster.ts +10 -1
- package/src/notifications/home-feed-side-effect.ts +4 -3
- package/src/notifications/notification-utils.ts +17 -19
- package/src/notifications/types.ts +7 -0
- package/src/oauth/AGENTS.md +5 -24
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +6 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
- package/src/prompts/persona-resolver.ts +2 -2
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/actor-trust-resolver.ts +12 -44
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/channel-approval-types.ts +18 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-verification-service.ts +4 -3
- package/src/runtime/invite-redemption-service.ts +3 -3
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +171 -5
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/workflow-routes.test.ts +4 -43
- package/src/runtime/routes/workflow-routes.ts +0 -28
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/schedule/inference-profile.ts +2 -14
- package/src/subagent/manager.ts +6 -0
- package/src/subagent/types.ts +6 -0
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/schedule/create.ts +3 -9
- package/src/tools/schedule/update.ts +2 -10
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +34 -0
- package/src/tools/subagent/spawn.ts +34 -9
- package/src/tools/tool-approval-handler.ts +1 -3
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/util/platform.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/run-manager.test.ts +0 -25
- package/src/workflows/run-manager.ts +2 -24
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
package/src/subagent/manager.ts
CHANGED
|
@@ -453,6 +453,9 @@ export class SubagentManager {
|
|
|
453
453
|
...(managed.state.config.overrideProfile
|
|
454
454
|
? { overrideProfile: managed.state.config.overrideProfile }
|
|
455
455
|
: {}),
|
|
456
|
+
...(managed.state.config.forceOverrideProfile
|
|
457
|
+
? { forceOverrideProfile: true }
|
|
458
|
+
: {}),
|
|
456
459
|
});
|
|
457
460
|
|
|
458
461
|
// Agent loop completed successfully.
|
|
@@ -641,6 +644,9 @@ export class SubagentManager {
|
|
|
641
644
|
...(managed.state.config.overrideProfile
|
|
642
645
|
? { overrideProfile: managed.state.config.overrideProfile }
|
|
643
646
|
: {}),
|
|
647
|
+
...(managed.state.config.forceOverrideProfile
|
|
648
|
+
? { forceOverrideProfile: true }
|
|
649
|
+
: {}),
|
|
644
650
|
})
|
|
645
651
|
.catch((err) => {
|
|
646
652
|
log.error({ subagentId, err }, "Subagent message processing failed");
|
package/src/subagent/types.ts
CHANGED
|
@@ -69,6 +69,12 @@ export interface SubagentConfig {
|
|
|
69
69
|
* profile, every spawned subagent inherits it automatically.
|
|
70
70
|
*/
|
|
71
71
|
overrideProfile?: string;
|
|
72
|
+
/**
|
|
73
|
+
* When true, the subagent's `overrideProfile` is an explicit spawn-time
|
|
74
|
+
* request and must float above call-site layers. Inherited parent profiles
|
|
75
|
+
* leave this unset so existing call-site precedence stays intact.
|
|
76
|
+
*/
|
|
77
|
+
forceOverrideProfile?: boolean;
|
|
72
78
|
/**
|
|
73
79
|
* Tool-use id of the `skill_execute` call that spawned this subagent.
|
|
74
80
|
* Forwarded into the `subagent_spawned` event so the client can anchor the
|
package/src/tools/AGENTS.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## New Non-Skill Tools Are Strongly Discouraged
|
|
4
4
|
|
|
5
|
-
**Prefer skills over new non-skill tool registrations.** Non-skill tools require approval from
|
|
5
|
+
**Prefer skills over new non-skill tool registrations.** Non-skill tools require approval from the core team.
|
|
6
6
|
|
|
7
7
|
Skills are the preferred approach for adding new capabilities — they are progressively disclosed into context, more portable, and can be iterated on independently. New non-skill tool registrations (`class ... implements Tool` + `registerTool()`) carry additional costs:
|
|
8
8
|
|
|
@@ -48,7 +48,7 @@ See [`assistant/docs/credential-execution-service.md`](../../docs/credential-exe
|
|
|
48
48
|
|
|
49
49
|
## If You Have Approval
|
|
50
50
|
|
|
51
|
-
If
|
|
51
|
+
If the core team has approved your new tool:
|
|
52
52
|
|
|
53
53
|
1. The pre-commit hook will block your commit by default
|
|
54
54
|
2. Use `git commit --no-verify` to bypass the hook
|
|
@@ -56,4 +56,4 @@ If Team Jarvis has approved your new tool:
|
|
|
56
56
|
|
|
57
57
|
## Questions?
|
|
58
58
|
|
|
59
|
-
Contact
|
|
59
|
+
Contact the core team before shipping a new tool.
|
|
@@ -1093,7 +1093,7 @@ export async function executeBrowserNavigate(
|
|
|
1093
1093
|
}
|
|
1094
1094
|
} else {
|
|
1095
1095
|
// Login / 2FA / OAuth - the agent should handle these itself
|
|
1096
|
-
// using browser operations +
|
|
1096
|
+
// using browser operations + stored credentials. Don't hand off.
|
|
1097
1097
|
lines.push("");
|
|
1098
1098
|
lines.push(formatAuthChallenge(challenge));
|
|
1099
1099
|
lines.push("");
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// failures and the user-facing copy we surface for them (ATL-727).
|
|
3
3
|
//
|
|
4
4
|
// This is a pure leaf module: it has NO imports from `daemon/`, `agent/`,
|
|
5
|
-
// `
|
|
5
|
+
// `clients/`, or any client/UI package. It may only import the logger and pino
|
|
6
6
|
// types (for the telemetry helper). Every web_search code path — native
|
|
7
7
|
// Anthropic handler, app-side providers, and the web client default — funnels
|
|
8
8
|
// failures through `classifyWebSearchFailure` so the same friendly message
|
|
@@ -173,7 +173,7 @@ export class PermissionChecker {
|
|
|
173
173
|
if (
|
|
174
174
|
context.forcePromptSideEffects &&
|
|
175
175
|
result.decision === "allow" &&
|
|
176
|
-
isSideEffectTool(name
|
|
176
|
+
isSideEffectTool(name)
|
|
177
177
|
) {
|
|
178
178
|
result.decision = "prompt";
|
|
179
179
|
result.reason = "Side-effect tool requires explicit approval";
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
2
|
-
import { getConfig } from "../../config/loader.js";
|
|
3
1
|
import { validateScheduleInferenceProfile } from "../../schedule/inference-profile.js";
|
|
4
2
|
import { formatIntegrationSummary } from "../../schedule/integration-status.js";
|
|
5
3
|
import { validateRruleSetLines } from "../../schedule/recurrence-engine.js";
|
|
@@ -121,13 +119,9 @@ export async function executeScheduleCreate(
|
|
|
121
119
|
};
|
|
122
120
|
}
|
|
123
121
|
} else if (mode === "workflow") {
|
|
124
|
-
// Workflow mode
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
// path and the settings route enforce the same shape.
|
|
128
|
-
if (!isAssistantFeatureFlagEnabled("workflows", getConfig())) {
|
|
129
|
-
return { content: "Error: workflows are not enabled.", isError: true };
|
|
130
|
-
}
|
|
122
|
+
// Workflow mode requires a saved workflow name — mirrors the HTTP route's
|
|
123
|
+
// create-side validation so the assistant-facing path and the settings route
|
|
124
|
+
// enforce the same shape.
|
|
131
125
|
if (!workflowName) {
|
|
132
126
|
return {
|
|
133
127
|
content:
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
2
|
-
import { getConfig } from "../../config/loader.js";
|
|
3
1
|
import { validateScheduleInferenceProfile } from "../../schedule/inference-profile.js";
|
|
4
2
|
import { validateRruleSetLines } from "../../schedule/recurrence-engine.js";
|
|
5
3
|
import {
|
|
@@ -205,8 +203,8 @@ export async function executeScheduleUpdate(
|
|
|
205
203
|
};
|
|
206
204
|
}
|
|
207
205
|
|
|
208
|
-
// Mirror the HTTP route: a schedule whose RESULTING mode is `workflow` must
|
|
209
|
-
//
|
|
206
|
+
// Mirror the HTTP route: a schedule whose RESULTING mode is `workflow` must
|
|
207
|
+
// carry a non-empty workflowName. Compute the post-update
|
|
210
208
|
// state (the update's value if present, else the persisted one) so both
|
|
211
209
|
// "switch to workflow without a name" and "clear the name on a workflow
|
|
212
210
|
// schedule" are rejected — otherwise the scheduler hits the `!job.workflowName`
|
|
@@ -217,12 +215,6 @@ export async function executeScheduleUpdate(
|
|
|
217
215
|
const resultingMode =
|
|
218
216
|
updates.mode !== undefined ? (updates.mode as string) : existing.mode;
|
|
219
217
|
if (resultingMode === "workflow") {
|
|
220
|
-
if (!isAssistantFeatureFlagEnabled("workflows", getConfig())) {
|
|
221
|
-
return {
|
|
222
|
-
content: "Error: workflows are not enabled.",
|
|
223
|
-
isError: true,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
218
|
const resultingWorkflowName =
|
|
227
219
|
updates.workflowName !== undefined
|
|
228
220
|
? ((updates.workflowName as string | null) ?? "")
|
|
@@ -25,22 +25,7 @@ const SIDE_EFFECT_TOOLS: ReadonlySet<string> = new Set([
|
|
|
25
25
|
* Returns `true` if the given tool name is classified as having side effects
|
|
26
26
|
* (i.e. it can modify the filesystem, execute arbitrary commands, or trigger
|
|
27
27
|
* external actions). Read-only and informational tools return `false`.
|
|
28
|
-
*
|
|
29
|
-
* For mixed-action tools (e.g. credential_store), the optional
|
|
30
|
-
* `input` parameter is inspected to distinguish mutating actions (create,
|
|
31
|
-
* update, cancel) from read-only ones (list, get).
|
|
32
28
|
*/
|
|
33
|
-
export function isSideEffectTool(
|
|
34
|
-
toolName
|
|
35
|
-
input?: Record<string, unknown>,
|
|
36
|
-
): boolean {
|
|
37
|
-
if (SIDE_EFFECT_TOOLS.has(toolName)) return true;
|
|
38
|
-
|
|
39
|
-
// Action-aware checks for mixed-action tools
|
|
40
|
-
if (toolName === "credential_store") {
|
|
41
|
-
const action = input?.action;
|
|
42
|
-
return action === "store" || action === "delete" || action === "prompt";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return false;
|
|
29
|
+
export function isSideEffectTool(toolName: string): boolean {
|
|
30
|
+
return SIDE_EFFECT_TOOLS.has(toolName);
|
|
46
31
|
}
|
|
@@ -62,6 +62,40 @@ export function resolveSkillExecuteInput(
|
|
|
62
62
|
return {};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Augment an inner-tool error with `skill_execute` envelope guidance when the
|
|
67
|
+
* call carried no inner parameters.
|
|
68
|
+
*
|
|
69
|
+
* {@link resolveSkillExecuteInput} rescues *misplaced* parameters (siblings, a
|
|
70
|
+
* JSON-encoded string). It cannot rescue a genuinely empty call — there is
|
|
71
|
+
* nothing to relocate — so the inner tool runs with `{}` and rejects it with a
|
|
72
|
+
* field-level message ("<field> is required") that says nothing about the
|
|
73
|
+
* envelope. Weak models then retry the identical empty shape, oscillating over
|
|
74
|
+
* whether parameters belong under `input`, as siblings, or as a JSON string.
|
|
75
|
+
*
|
|
76
|
+
* Appending the canonical envelope shape gives the next attempt a concrete
|
|
77
|
+
* template. Fires only when the resolved inner input was empty AND the tool
|
|
78
|
+
* errored, so well-formed calls and tools that legitimately accept no
|
|
79
|
+
* parameters are untouched.
|
|
80
|
+
*/
|
|
81
|
+
export function augmentSkillExecuteError(
|
|
82
|
+
toolName: string,
|
|
83
|
+
resolvedInput: Record<string, unknown>,
|
|
84
|
+
result: ToolExecutionResult,
|
|
85
|
+
): ToolExecutionResult {
|
|
86
|
+
if (!result.isError || Object.keys(resolvedInput).length > 0) return result;
|
|
87
|
+
|
|
88
|
+
const guidance =
|
|
89
|
+
`\n\nThis skill_execute call carried no parameters for "${toolName}". ` +
|
|
90
|
+
`Put the tool's parameters inside \`input\` as a JSON object — not as ` +
|
|
91
|
+
`siblings of \`tool\`, and not as a JSON-encoded string. For example: ` +
|
|
92
|
+
`{"tool": "${toolName}", "input": { /* the tool's parameters */ }, ` +
|
|
93
|
+
`"activity": "..."}. The skill's instructions (from skill_load) list ` +
|
|
94
|
+
`"${toolName}"'s required fields.`;
|
|
95
|
+
|
|
96
|
+
return { ...result, content: result.content + guidance };
|
|
97
|
+
}
|
|
98
|
+
|
|
65
99
|
export const skillExecuteTool = {
|
|
66
100
|
name: "skill_execute",
|
|
67
101
|
description:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateInferenceProfileKey } from "../../config/inference-profile-validation.js";
|
|
1
2
|
import { findConversation } from "../../daemon/conversation-registry.js";
|
|
2
3
|
import { getConversationOverrideProfile } from "../../memory/conversation-crud.js";
|
|
3
4
|
import type { Message } from "../../providers/types.js";
|
|
@@ -14,6 +15,7 @@ export async function executeSubagentSpawn(
|
|
|
14
15
|
const extraContext = input.context as string | undefined;
|
|
15
16
|
const fork = input.fork === true;
|
|
16
17
|
const role = (input.role as string | undefined) ?? undefined;
|
|
18
|
+
const inferenceProfile = input.inference_profile;
|
|
17
19
|
|
|
18
20
|
// For fork mode, sendResultToUser defaults to false unless explicitly set to true.
|
|
19
21
|
// For regular mode, sendResultToUser defaults to true (existing behavior).
|
|
@@ -28,6 +30,26 @@ export async function executeSubagentSpawn(
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
let requestedOverrideProfile: string | undefined;
|
|
34
|
+
let forceOverrideProfile = false;
|
|
35
|
+
if (inferenceProfile !== undefined) {
|
|
36
|
+
if (typeof inferenceProfile !== "string") {
|
|
37
|
+
return {
|
|
38
|
+
content: "Error: inference_profile must be a string",
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const profileError = validateInferenceProfileKey(inferenceProfile);
|
|
43
|
+
if (profileError) {
|
|
44
|
+
return {
|
|
45
|
+
content: `Error: ${profileError}`,
|
|
46
|
+
isError: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
requestedOverrideProfile = inferenceProfile;
|
|
50
|
+
forceOverrideProfile = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
const manager = getSubagentManager();
|
|
32
54
|
const sendToClient = context.sendToClient as
|
|
33
55
|
| ((msg: { type: string; [key: string]: unknown }) => void)
|
|
@@ -75,16 +97,18 @@ export async function executeSubagentSpawn(
|
|
|
75
97
|
// `SubagentManager.spawn` forwards it back into the subagent's
|
|
76
98
|
// `runAgentLoop` call as `options.overrideProfile`.
|
|
77
99
|
//
|
|
78
|
-
// Prefer the per-turn
|
|
79
|
-
// `
|
|
80
|
-
// row read so nested spawns inherit
|
|
81
|
-
// own conversation row never has
|
|
82
|
-
//
|
|
83
|
-
// were set,
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// agent-loop
|
|
100
|
+
// Prefer an explicit spawn-time profile, then the per-turn
|
|
101
|
+
// `context.overrideProfile` (populated by `runAgentLoopImpl` from its
|
|
102
|
+
// resolved `turnOverrideProfile`) over a row read so nested spawns inherit
|
|
103
|
+
// correctly. The current subagent's own conversation row never has
|
|
104
|
+
// `inferenceProfile` set — its override arrived via in-memory
|
|
105
|
+
// `SubagentConfig.overrideProfile` — and even if it were set,
|
|
106
|
+
// `getConversationOverrideProfile` short-circuits for background
|
|
107
|
+
// conversations and returns `undefined`. Falling back to the row read
|
|
108
|
+
// preserves behavior for tool calls that originate outside an agent-loop
|
|
109
|
+
// turn.
|
|
87
110
|
const inheritedOverrideProfile =
|
|
111
|
+
requestedOverrideProfile ??
|
|
88
112
|
context.overrideProfile ??
|
|
89
113
|
getConversationOverrideProfile(context.conversationId);
|
|
90
114
|
|
|
@@ -102,6 +126,7 @@ export async function executeSubagentSpawn(
|
|
|
102
126
|
...(inheritedOverrideProfile
|
|
103
127
|
? { overrideProfile: inheritedOverrideProfile }
|
|
104
128
|
: {}),
|
|
129
|
+
...(forceOverrideProfile ? { forceOverrideProfile: true } : {}),
|
|
105
130
|
...(context.toolUseId ? { parentToolUseId: context.toolUseId } : {}),
|
|
106
131
|
...forkFields,
|
|
107
132
|
},
|
|
@@ -157,7 +157,6 @@ const UI_SURFACE_TOOLS = new Set(["ui_show", "ui_update", "ui_dismiss"]);
|
|
|
157
157
|
|
|
158
158
|
function requiresGuardianApprovalForActor(
|
|
159
159
|
toolName: string,
|
|
160
|
-
input: Record<string, unknown>,
|
|
161
160
|
executionTarget: ExecutionTarget,
|
|
162
161
|
): boolean {
|
|
163
162
|
// UI surface tools are passive, user-visible operations (cards, forms,
|
|
@@ -170,7 +169,7 @@ function requiresGuardianApprovalForActor(
|
|
|
170
169
|
// Side-effect tools always require guardian approval for untrusted actors.
|
|
171
170
|
// Read-only host execution is also blocked because it can leak sensitive
|
|
172
171
|
// local information (e.g. shell/file reads).
|
|
173
|
-
return isSideEffectTool(toolName
|
|
172
|
+
return isSideEffectTool(toolName) || executionTarget === "host";
|
|
174
173
|
}
|
|
175
174
|
|
|
176
175
|
function guardianApprovalDeniedMessage(
|
|
@@ -320,7 +319,6 @@ export class ToolApprovalHandler {
|
|
|
320
319
|
|
|
321
320
|
const guardianApprovalRequired = requiresGuardianApprovalForActor(
|
|
322
321
|
name,
|
|
323
|
-
input,
|
|
324
322
|
executionTarget,
|
|
325
323
|
);
|
|
326
324
|
|
|
@@ -15,7 +15,6 @@ import { askQuestionTool } from "./ask-question/ask-question-tool.js";
|
|
|
15
15
|
import { makeAuthenticatedRequestTool } from "./credential-execution/make-authenticated-request.js";
|
|
16
16
|
import { manageSecureCommandTool } from "./credential-execution/manage-secure-command-tool.js";
|
|
17
17
|
import { runAuthenticatedCommandTool } from "./credential-execution/run-authenticated-command.js";
|
|
18
|
-
import { credentialStoreTool } from "./credentials/vault.js";
|
|
19
18
|
import { fileEditTool } from "./filesystem/edit.js";
|
|
20
19
|
import { fileListTool } from "./filesystem/list.js";
|
|
21
20
|
import { fileReadTool } from "./filesystem/read.js";
|
|
@@ -90,7 +89,6 @@ export const explicitTools: ToolDefinition[] = [
|
|
|
90
89
|
// Always-explicit tools
|
|
91
90
|
rememberTool,
|
|
92
91
|
recallTool,
|
|
93
|
-
credentialStoreTool,
|
|
94
92
|
notifyParentTool,
|
|
95
93
|
askQuestionTool,
|
|
96
94
|
// NOTE: external skill tools (registered via registerExternalTools in
|
|
@@ -59,6 +59,14 @@ function proxyExecute(toolName: string) {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
if (toolName === "ui_update" && isEmptyUpdate(input)) {
|
|
63
|
+
return {
|
|
64
|
+
content:
|
|
65
|
+
'Error: ui_update received an empty `data` payload, so the surface was unchanged — the user still sees its previous state. The provided data is merged into the surface\'s current data, and merging nothing is a no-op. To advance a task_progress card, send the full step list: ui_update { surface_id: "<id>", data: { templateData: { steps: [{ label: "<step>", status: "completed" }, { label: "<step>", status: "in_progress" }] } } }. Resend ui_update with the fields you intend to change under `data`.',
|
|
66
|
+
isError: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
62
70
|
if (!context.proxyToolResolver) {
|
|
63
71
|
return {
|
|
64
72
|
content: `No proxy resolver configured for proxy tool "${toolName}". This tool requires an external resolver (e.g. a connected macOS client).`,
|
|
@@ -98,6 +106,36 @@ function isTaskProgressCardShow(input: Record<string, unknown>): boolean {
|
|
|
98
106
|
return data?.template === "task_progress";
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
/**
|
|
110
|
+
* A `ui_update` whose `data` merge would change nothing: missing, not an
|
|
111
|
+
* object, or containing only (recursively) empty objects. Merging such a
|
|
112
|
+
* payload is a silent no-op — the surface keeps its prior state while the
|
|
113
|
+
* client still reports "Surface updated" — so the model never learns its
|
|
114
|
+
* update was hollow and a live card (e.g. task_progress) appears frozen.
|
|
115
|
+
* Arrays (e.g. `templateData.steps`) and any non-empty primitive leaf count
|
|
116
|
+
* as content.
|
|
117
|
+
*/
|
|
118
|
+
function isEmptyUpdate(input: Record<string, unknown>): boolean {
|
|
119
|
+
const data = asRecord(input.data);
|
|
120
|
+
return data === null || !hasContent(data);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function hasContent(value: unknown): boolean {
|
|
124
|
+
if (value === null || value === undefined) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(value)) {
|
|
128
|
+
return value.length > 0;
|
|
129
|
+
}
|
|
130
|
+
if (typeof value === "object") {
|
|
131
|
+
return Object.values(value).some(hasContent);
|
|
132
|
+
}
|
|
133
|
+
if (typeof value === "string") {
|
|
134
|
+
return value.trim().length > 0;
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
101
139
|
function isEmptyDynamicPage(input: Record<string, unknown>): boolean {
|
|
102
140
|
if (input.surface_type !== "dynamic_page") {
|
|
103
141
|
return false;
|
|
@@ -311,7 +349,7 @@ export const uiShowTool = {
|
|
|
311
349
|
// ui_update
|
|
312
350
|
// ---------------------------------------------------------------------------
|
|
313
351
|
|
|
314
|
-
const uiUpdateTool = {
|
|
352
|
+
export const uiUpdateTool = {
|
|
315
353
|
name: "ui_update",
|
|
316
354
|
description:
|
|
317
355
|
"Update an existing surface's data. The provided data object is merged into the surface's current data.\n" +
|
|
@@ -11,11 +11,9 @@ mock.module("../../util/logger.js", () => ({
|
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
13
|
// ── Mutable mock state ────────────────────────────────────────────────
|
|
14
|
-
// `
|
|
15
|
-
//
|
|
16
|
-
// records the args of the last `start()` call for assertion.
|
|
14
|
+
// `configThrows` simulates config not yet loaded (test-setup race). The
|
|
15
|
+
// run-manager mock records the args of the last `start()` call for assertion.
|
|
17
16
|
|
|
18
|
-
let flagEnabled = true;
|
|
19
17
|
let configThrows = false;
|
|
20
18
|
|
|
21
19
|
const realLoader = await import("../../config/loader.js");
|
|
@@ -35,13 +33,6 @@ mock.module("../../config/loader.js", () => ({
|
|
|
35
33
|
}) as unknown as ReturnType<typeof realLoader.loadConfig>,
|
|
36
34
|
}));
|
|
37
35
|
|
|
38
|
-
const realFlags = await import("../../config/assistant-feature-flags.js");
|
|
39
|
-
mock.module("../../config/assistant-feature-flags.js", () => ({
|
|
40
|
-
...realFlags,
|
|
41
|
-
isAssistantFeatureFlagEnabled: (key: string) =>
|
|
42
|
-
key === "workflows" ? flagEnabled : false,
|
|
43
|
-
}));
|
|
44
|
-
|
|
45
36
|
// No live conversation in tests — the tool falls back to a synthetic trust
|
|
46
37
|
// context built from the tool context's trustClass.
|
|
47
38
|
const realRegistry = await import("../../daemon/conversation-registry.js");
|
|
@@ -92,7 +83,6 @@ function makeContext(): Parameters<typeof executeRunWorkflow>[1] {
|
|
|
92
83
|
}
|
|
93
84
|
|
|
94
85
|
beforeEach(() => {
|
|
95
|
-
flagEnabled = true;
|
|
96
86
|
configThrows = false;
|
|
97
87
|
startThrows = null;
|
|
98
88
|
lastStartArgs = null;
|
|
@@ -105,10 +95,10 @@ beforeEach(() => {
|
|
|
105
95
|
});
|
|
106
96
|
|
|
107
97
|
describe("workflow tools are served by the workflows skill", () => {
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
98
|
+
// run_workflow / manage_workflows are served by the `workflows` bundled skill
|
|
99
|
+
// (loaded via skill_load, invoked via skill_execute), not the always-on tool
|
|
100
|
+
// manifest. The skill manifest is the source of truth; assert it declares both
|
|
101
|
+
// as in-process host tools.
|
|
112
102
|
test("the workflows skill TOOLS.json declares both host-executed tools", async () => {
|
|
113
103
|
const { readFileSync } = await import("node:fs");
|
|
114
104
|
const { join } = await import("node:path");
|
|
@@ -211,13 +201,13 @@ describe("run_workflow launch", () => {
|
|
|
211
201
|
});
|
|
212
202
|
|
|
213
203
|
test("surfaces a run-manager start error as a tool error", async () => {
|
|
214
|
-
startThrows = new Error("
|
|
204
|
+
startThrows = new Error("run manager exploded");
|
|
215
205
|
const res = await executeRunWorkflow(
|
|
216
206
|
{ script: "export const meta = {};" },
|
|
217
207
|
makeContext(),
|
|
218
208
|
);
|
|
219
209
|
expect(res.isError).toBe(true);
|
|
220
|
-
expect(res.content).toContain("
|
|
210
|
+
expect(res.content).toContain("run manager exploded");
|
|
221
211
|
});
|
|
222
212
|
});
|
|
223
213
|
|
package/src/util/platform.ts
CHANGED
|
@@ -402,8 +402,8 @@ export function getSkillRuntimePath(
|
|
|
402
402
|
*
|
|
403
403
|
* Resolution order:
|
|
404
404
|
*
|
|
405
|
-
* 1. macOS `.app` bundle: `Contents/Resources/bun` —
|
|
406
|
-
*
|
|
405
|
+
* 1. macOS `.app` bundle: `Contents/Resources/bun` — bundled at a version
|
|
406
|
+
* that matches `.tool-versions`.
|
|
407
407
|
* 2. Next-to-binary: `<execDir>/bun` for Docker/generic compiled layouts
|
|
408
408
|
* that stage a bun binary alongside the daemon (PR 29 wires this up).
|
|
409
409
|
*
|
|
@@ -29,9 +29,8 @@
|
|
|
29
29
|
* (If host execution is ever wanted, the deliberate path is to thread the
|
|
30
30
|
* originating tool context through the engine — not to relax this gate.)
|
|
31
31
|
*
|
|
32
|
-
* This module is pure logic: it performs no
|
|
33
|
-
*
|
|
34
|
-
* callers, not this code.
|
|
32
|
+
* This module is pure logic: it performs no I/O beyond the synchronous
|
|
33
|
+
* tool-registry lookup.
|
|
35
34
|
*/
|
|
36
35
|
|
|
37
36
|
import { z } from "zod";
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
WorkflowRunCapError,
|
|
23
23
|
WorkflowRunManager,
|
|
24
24
|
type WorkflowRunManagerDeps,
|
|
25
|
-
WorkflowsDisabledError,
|
|
26
25
|
} from "./run-manager.js";
|
|
27
26
|
|
|
28
27
|
const TRUST: TrustContext = {
|
|
@@ -144,7 +143,6 @@ interface EngineResult {
|
|
|
144
143
|
* deterministically), an in-memory journal, and spies for broadcast + wake.
|
|
145
144
|
*/
|
|
146
145
|
function makeHarness(opts?: {
|
|
147
|
-
flagEnabled?: boolean;
|
|
148
146
|
maxConcurrentRuns?: number;
|
|
149
147
|
/** Custom engine impl; defaults to the deferred-resolver fake. */
|
|
150
148
|
engine?: WorkflowRunManagerDeps["executeWorkflow"];
|
|
@@ -190,7 +188,6 @@ function makeHarness(opts?: {
|
|
|
190
188
|
}) as unknown as WorkflowRunManagerDeps["leafRunner"],
|
|
191
189
|
journal: fake.journal,
|
|
192
190
|
getConfig: () => makeConfig(opts?.maxConcurrentRuns),
|
|
193
|
-
isFlagEnabled: () => opts?.flagEnabled ?? true,
|
|
194
191
|
wake: (async (wakeOpts) => {
|
|
195
192
|
wakes.push({
|
|
196
193
|
conversationId: wakeOpts.conversationId,
|
|
@@ -227,22 +224,6 @@ function makeHarness(opts?: {
|
|
|
227
224
|
};
|
|
228
225
|
}
|
|
229
226
|
|
|
230
|
-
describe("WorkflowRunManager.start — flag gate", () => {
|
|
231
|
-
test("flag off → start throws and the engine is never invoked", () => {
|
|
232
|
-
const h = makeHarness({ flagEnabled: false });
|
|
233
|
-
expect(() =>
|
|
234
|
-
h.manager.start({
|
|
235
|
-
scriptSource: "export const meta = { name: 'x', description: 'y' }",
|
|
236
|
-
args: {},
|
|
237
|
-
manifest: { tools: [], hostFunctions: [], persona: false },
|
|
238
|
-
trustContext: TRUST,
|
|
239
|
-
}),
|
|
240
|
-
).toThrow(WorkflowsDisabledError);
|
|
241
|
-
expect(h.executeCalls).toHaveLength(0);
|
|
242
|
-
expect(h.fake.rows.size).toBe(0);
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
|
|
246
227
|
describe("WorkflowRunManager.start — concurrent-run cap", () => {
|
|
247
228
|
test("the (N+1)th concurrent start is rejected", () => {
|
|
248
229
|
const h = makeHarness({ maxConcurrentRuns: 2 });
|
|
@@ -702,10 +683,4 @@ describe("WorkflowRunManager.resume", () => {
|
|
|
702
683
|
|
|
703
684
|
expect(() => h.manager.resume("run-x")).toThrow(WorkflowRunCapError);
|
|
704
685
|
});
|
|
705
|
-
|
|
706
|
-
test("resume is rejected when the flag is off", () => {
|
|
707
|
-
const h = makeHarness({ flagEnabled: false });
|
|
708
|
-
seedRun(h, { id: "run-x", status: "interrupted" });
|
|
709
|
-
expect(() => h.manager.resume("run-x")).toThrow(WorkflowsDisabledError);
|
|
710
|
-
});
|
|
711
686
|
});
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
* and routes (later PRs) drive. It owns everything the raw {@link executeWorkflow}
|
|
6
6
|
* engine deliberately does NOT:
|
|
7
7
|
*
|
|
8
|
-
* - **Feature-flag gate.** `start` hard-fails with {@link WorkflowsDisabledError}
|
|
9
|
-
* when the `workflows` flag is off, BEFORE any engine code path is reachable.
|
|
10
8
|
* - **Concurrent-run cap.** At most `config.workflows.maxConcurrentRuns` runs
|
|
11
9
|
* may be in flight; the (N+1)th `start` throws {@link WorkflowRunCapError}.
|
|
12
10
|
* - **Async launch.** `start` resolves capabilities, creates the journal run
|
|
@@ -28,7 +26,6 @@
|
|
|
28
26
|
|
|
29
27
|
import { createHash, randomUUID } from "node:crypto";
|
|
30
28
|
|
|
31
|
-
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
32
29
|
import { getConfig } from "../config/loader.js";
|
|
33
30
|
import type { AssistantConfig } from "../config/schema.js";
|
|
34
31
|
import {
|
|
@@ -58,15 +55,6 @@ const log = getLogger("workflow-run-manager");
|
|
|
58
55
|
/** Source tag for the completion wake (shows up in the wake's structured log). */
|
|
59
56
|
const WORKFLOW_WAKE_SOURCE = "workflow_completed";
|
|
60
57
|
|
|
61
|
-
/** Thrown by `start` when the `workflows` feature flag is disabled. */
|
|
62
|
-
export class WorkflowsDisabledError extends Error {
|
|
63
|
-
readonly code = "workflows_disabled" as const;
|
|
64
|
-
constructor() {
|
|
65
|
-
super("Workflows are not enabled.");
|
|
66
|
-
this.name = "WorkflowsDisabledError";
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
58
|
/** Thrown by `start` when the concurrent-run cap is already reached. */
|
|
71
59
|
export class WorkflowRunCapError extends Error {
|
|
72
60
|
readonly code = "workflow_run_cap_exceeded" as const;
|
|
@@ -141,7 +129,6 @@ export interface WorkflowRunManagerDeps {
|
|
|
141
129
|
leafRunner: typeof runLeaf;
|
|
142
130
|
journal: typeof journalStore;
|
|
143
131
|
getConfig: () => AssistantConfig;
|
|
144
|
-
isFlagEnabled: (config: AssistantConfig) => boolean;
|
|
145
132
|
wake: typeof wakeAgentForOpportunity;
|
|
146
133
|
broadcast: typeof broadcastMessage;
|
|
147
134
|
newRunId: () => string;
|
|
@@ -155,8 +142,6 @@ function defaultDeps(): WorkflowRunManagerDeps {
|
|
|
155
142
|
leafRunner: runLeaf,
|
|
156
143
|
journal: journalStore,
|
|
157
144
|
getConfig,
|
|
158
|
-
isFlagEnabled: (config) =>
|
|
159
|
-
isAssistantFeatureFlagEnabled("workflows", config),
|
|
160
145
|
wake: wakeAgentForOpportunity,
|
|
161
146
|
broadcast: broadcastMessage,
|
|
162
147
|
newRunId: () => randomUUID(),
|
|
@@ -178,18 +163,14 @@ export class WorkflowRunManager {
|
|
|
178
163
|
}
|
|
179
164
|
|
|
180
165
|
/**
|
|
181
|
-
* Launch a workflow run. Gates on the
|
|
182
|
-
*
|
|
166
|
+
* Launch a workflow run. Gates on the concurrent-run cap (which throws before
|
|
167
|
+
* any engine code is reachable), resolves capabilities,
|
|
183
168
|
* creates the journal run row, and kicks off {@link executeWorkflow}
|
|
184
169
|
* asynchronously. Returns the `runId` immediately — completion is surfaced via
|
|
185
170
|
* events and a conversation wake.
|
|
186
171
|
*/
|
|
187
172
|
start(opts: StartWorkflowOptions): { runId: string } {
|
|
188
173
|
const config = this.deps.getConfig();
|
|
189
|
-
if (!this.deps.isFlagEnabled(config)) {
|
|
190
|
-
throw new WorkflowsDisabledError();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
174
|
const limit = config.workflows.maxConcurrentRuns;
|
|
194
175
|
if (this.inflight.size >= limit) {
|
|
195
176
|
throw new WorkflowRunCapError(limit);
|
|
@@ -282,9 +263,6 @@ export class WorkflowRunManager {
|
|
|
282
263
|
*/
|
|
283
264
|
resume(runId: string): { runId: string } {
|
|
284
265
|
const config = this.deps.getConfig();
|
|
285
|
-
if (!this.deps.isFlagEnabled(config)) {
|
|
286
|
-
throw new WorkflowsDisabledError();
|
|
287
|
-
}
|
|
288
266
|
|
|
289
267
|
if (this.inflight.has(runId)) {
|
|
290
268
|
throw new WorkflowResumeNotPossibleError(runId, "in_flight");
|