stagent 0.9.5 → 0.10.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 +5 -42
- package/dist/cli.js +42 -18
- package/docs/.coverage-gaps.json +13 -55
- package/docs/.last-generated +1 -1
- package/docs/features/provider-runtimes.md +4 -0
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/tables.md +9 -2
- package/docs/features/workflows.md +10 -4
- package/docs/journeys/developer.md +15 -1
- package/docs/journeys/personal-use.md +21 -4
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +0 -21
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +6 -9
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
- package/src/components/chat/chat-command-popover.tsx +2 -2
- package/src/components/chat/chat-input.tsx +2 -3
- package/src/components/chat/chat-session-provider.tsx +720 -0
- package/src/components/chat/chat-shell.tsx +92 -401
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +4 -5
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +19 -0
- package/src/components/tasks/task-card.tsx +26 -3
- package/src/components/tasks/task-chip-bar.tsx +24 -0
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/claude-agent.ts +155 -18
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +29 -60
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/codex-engine.ts +16 -17
- package/src/lib/chat/context-builder.ts +5 -3
- package/src/lib/chat/engine.ts +50 -3
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/stagent-tools.ts +1 -0
- package/src/lib/chat/stream-telemetry.ts +132 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +26 -1
- package/src/lib/chat/tool-catalog.ts +2 -1
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +39 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +84 -20
- package/src/lib/chat/tools/workflow-tools.ts +234 -32
- package/src/lib/constants/settings.ts +8 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +20 -15
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +45 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +68 -23
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +78 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +153 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +232 -13
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/types.ts +10 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +207 -4
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- package/src/lib/validators/license.ts +0 -33
|
@@ -3,12 +3,13 @@ import { z } from "zod";
|
|
|
3
3
|
import { db } from "@/lib/db";
|
|
4
4
|
import { tasks } from "@/lib/db/schema";
|
|
5
5
|
import { eq, and, desc } from "drizzle-orm";
|
|
6
|
-
import { ok, err, type ToolContext } from "./helpers";
|
|
6
|
+
import { ok, err, resolveEntityId, type ToolContext } from "./helpers";
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_AGENT_RUNTIME,
|
|
9
9
|
isAgentRuntimeId,
|
|
10
10
|
SUPPORTED_AGENT_RUNTIMES,
|
|
11
11
|
} from "@/lib/agents/runtime/catalog";
|
|
12
|
+
import { getProfile, listProfiles } from "@/lib/agents/profiles/registry";
|
|
12
13
|
|
|
13
14
|
const VALID_TASK_STATUSES = [
|
|
14
15
|
"planned",
|
|
@@ -19,6 +20,25 @@ const VALID_TASK_STATUSES = [
|
|
|
19
20
|
"cancelled",
|
|
20
21
|
] as const;
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Zod refinement shared by create_task and update_task for the agentProfile
|
|
25
|
+
* field. Returns true for valid registered profile IDs. The error message
|
|
26
|
+
* lists a truncated sample of valid IDs from the registry so operators can
|
|
27
|
+
* self-correct without cross-referencing docs.
|
|
28
|
+
*/
|
|
29
|
+
function isValidAgentProfile(id: string): boolean {
|
|
30
|
+
return getProfile(id) !== undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function agentProfileErrorMessage(invalid: string): string {
|
|
34
|
+
const valid = listProfiles()
|
|
35
|
+
.map((p) => p.id)
|
|
36
|
+
.sort();
|
|
37
|
+
const sample = valid.slice(0, 8).join(", ");
|
|
38
|
+
const more = valid.length > 8 ? `, and ${valid.length - 8} more` : "";
|
|
39
|
+
return `Invalid agentProfile "${invalid}". Valid profiles: ${sample}${more}. Run list_profiles (or inspect ~/.claude/skills/) to see the full set.`;
|
|
40
|
+
}
|
|
41
|
+
|
|
22
42
|
export function taskTools(ctx: ToolContext) {
|
|
23
43
|
return [
|
|
24
44
|
defineTool(
|
|
@@ -51,6 +71,14 @@ export function taskTools(ctx: ToolContext) {
|
|
|
51
71
|
.orderBy(tasks.priority, desc(tasks.createdAt))
|
|
52
72
|
.limit(50);
|
|
53
73
|
|
|
74
|
+
if (result.length === 0 && effectiveProjectId) {
|
|
75
|
+
return ok({
|
|
76
|
+
tasks: [],
|
|
77
|
+
note: `No tasks found in project ${effectiveProjectId}. ` +
|
|
78
|
+
`Use projectId: null to list tasks from any project, ` +
|
|
79
|
+
`or get_task <id> to look up a specific task directly.`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
54
82
|
return ok(result);
|
|
55
83
|
} catch (e) {
|
|
56
84
|
return err(e instanceof Error ? e.message : "Failed to list tasks");
|
|
@@ -90,13 +118,19 @@ export function taskTools(ctx: ToolContext) {
|
|
|
90
118
|
),
|
|
91
119
|
agentProfile: z
|
|
92
120
|
.string()
|
|
121
|
+
.refine(isValidAgentProfile, {
|
|
122
|
+
message: "Invalid agentProfile (not in profile registry). See list_profiles.",
|
|
123
|
+
})
|
|
93
124
|
.optional()
|
|
94
125
|
.describe(
|
|
95
|
-
"Agent profile ID (e.g. general, code-reviewer, researcher
|
|
126
|
+
"Agent profile ID (e.g. general, code-reviewer, researcher). Validated against the profile registry."
|
|
96
127
|
),
|
|
97
128
|
},
|
|
98
129
|
async (args) => {
|
|
99
130
|
try {
|
|
131
|
+
if (args.agentProfile !== undefined && !isValidAgentProfile(args.agentProfile)) {
|
|
132
|
+
return err(agentProfileErrorMessage(args.agentProfile));
|
|
133
|
+
}
|
|
100
134
|
if (args.assignedAgent && !isAgentRuntimeId(args.assignedAgent)) {
|
|
101
135
|
return err(
|
|
102
136
|
`Invalid runtime "${args.assignedAgent}". Valid: ${SUPPORTED_AGENT_RUNTIMES.join(", ")}`
|
|
@@ -162,13 +196,23 @@ export function taskTools(ctx: ToolContext) {
|
|
|
162
196
|
),
|
|
163
197
|
agentProfile: z
|
|
164
198
|
.string()
|
|
199
|
+
.refine(isValidAgentProfile, {
|
|
200
|
+
message: "Invalid agentProfile (not in profile registry). See list_profiles.",
|
|
201
|
+
})
|
|
165
202
|
.optional()
|
|
166
203
|
.describe(
|
|
167
|
-
"Agent profile ID (e.g. general, code-reviewer, researcher
|
|
204
|
+
"Agent profile ID (e.g. general, code-reviewer, researcher). Validated against the profile registry."
|
|
168
205
|
),
|
|
169
206
|
},
|
|
170
207
|
async (args) => {
|
|
171
208
|
try {
|
|
209
|
+
const resolved = await resolveEntityId(tasks, tasks.id, args.taskId);
|
|
210
|
+
if ("error" in resolved) return err(resolved.error);
|
|
211
|
+
const taskId = resolved.id;
|
|
212
|
+
|
|
213
|
+
if (args.agentProfile !== undefined && !isValidAgentProfile(args.agentProfile)) {
|
|
214
|
+
return err(agentProfileErrorMessage(args.agentProfile));
|
|
215
|
+
}
|
|
172
216
|
if (args.assignedAgent && !isAgentRuntimeId(args.assignedAgent)) {
|
|
173
217
|
return err(
|
|
174
218
|
`Invalid runtime "${args.assignedAgent}". Valid: ${SUPPORTED_AGENT_RUNTIMES.join(", ")}`
|
|
@@ -178,10 +222,10 @@ export function taskTools(ctx: ToolContext) {
|
|
|
178
222
|
const existing = await db
|
|
179
223
|
.select()
|
|
180
224
|
.from(tasks)
|
|
181
|
-
.where(eq(tasks.id,
|
|
225
|
+
.where(eq(tasks.id, taskId))
|
|
182
226
|
.get();
|
|
183
227
|
|
|
184
|
-
if (!existing) return err(`Task not found: ${
|
|
228
|
+
if (!existing) return err(`Task not found: ${taskId}`);
|
|
185
229
|
|
|
186
230
|
const updates: Record<string, unknown> = { updatedAt: new Date() };
|
|
187
231
|
if (args.title !== undefined) updates.title = args.title;
|
|
@@ -197,12 +241,12 @@ export function taskTools(ctx: ToolContext) {
|
|
|
197
241
|
await db
|
|
198
242
|
.update(tasks)
|
|
199
243
|
.set(updates)
|
|
200
|
-
.where(eq(tasks.id,
|
|
244
|
+
.where(eq(tasks.id, taskId));
|
|
201
245
|
|
|
202
246
|
const [task] = await db
|
|
203
247
|
.select()
|
|
204
248
|
.from(tasks)
|
|
205
|
-
.where(eq(tasks.id,
|
|
249
|
+
.where(eq(tasks.id, taskId));
|
|
206
250
|
|
|
207
251
|
ctx.onToolResult?.("update_task", task);
|
|
208
252
|
return ok(task);
|
|
@@ -220,13 +264,17 @@ export function taskTools(ctx: ToolContext) {
|
|
|
220
264
|
},
|
|
221
265
|
async (args) => {
|
|
222
266
|
try {
|
|
267
|
+
const resolved = await resolveEntityId(tasks, tasks.id, args.taskId);
|
|
268
|
+
if ("error" in resolved) return err(resolved.error);
|
|
269
|
+
const taskId = resolved.id;
|
|
270
|
+
|
|
223
271
|
const task = await db
|
|
224
272
|
.select()
|
|
225
273
|
.from(tasks)
|
|
226
|
-
.where(eq(tasks.id,
|
|
274
|
+
.where(eq(tasks.id, taskId))
|
|
227
275
|
.get();
|
|
228
276
|
|
|
229
|
-
if (!task) return err(`Task not found: ${
|
|
277
|
+
if (!task) return err(`Task not found: ${taskId}`);
|
|
230
278
|
ctx.onToolResult?.("get_task", task);
|
|
231
279
|
return ok(task);
|
|
232
280
|
} catch (e) {
|
|
@@ -249,6 +297,10 @@ export function taskTools(ctx: ToolContext) {
|
|
|
249
297
|
},
|
|
250
298
|
async (args) => {
|
|
251
299
|
try {
|
|
300
|
+
const resolved = await resolveEntityId(tasks, tasks.id, args.taskId);
|
|
301
|
+
if ("error" in resolved) return err(resolved.error);
|
|
302
|
+
const taskId = resolved.id;
|
|
303
|
+
|
|
252
304
|
if (args.assignedAgent && !isAgentRuntimeId(args.assignedAgent)) {
|
|
253
305
|
return err(
|
|
254
306
|
`Invalid runtime "${args.assignedAgent}". Valid: ${SUPPORTED_AGENT_RUNTIMES.join(", ")}`
|
|
@@ -258,26 +310,34 @@ export function taskTools(ctx: ToolContext) {
|
|
|
258
310
|
const task = await db
|
|
259
311
|
.select()
|
|
260
312
|
.from(tasks)
|
|
261
|
-
.where(eq(tasks.id,
|
|
313
|
+
.where(eq(tasks.id, taskId))
|
|
262
314
|
.get();
|
|
263
315
|
|
|
264
|
-
if (!task) return err(`Task not found: ${
|
|
316
|
+
if (!task) return err(`Task not found: ${taskId}`);
|
|
265
317
|
if (task.status === "running") return err("Task is already running");
|
|
266
318
|
|
|
319
|
+
if (task.agentProfile && !isValidAgentProfile(task.agentProfile)) {
|
|
320
|
+
return err(
|
|
321
|
+
`Task ${taskId} has an invalid agentProfile "${task.agentProfile}" (not in profile registry). ` +
|
|
322
|
+
`Fix with update_task { taskId, agentProfile: "<valid-id>" } before retrying. ` +
|
|
323
|
+
agentProfileErrorMessage(task.agentProfile).split(". ").slice(1).join(". ")
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
267
327
|
const runtimeId = args.assignedAgent ?? task.assignedAgent ?? DEFAULT_AGENT_RUNTIME;
|
|
268
328
|
|
|
269
329
|
// Set status to queued
|
|
270
330
|
await db
|
|
271
331
|
.update(tasks)
|
|
272
332
|
.set({ status: "queued", assignedAgent: runtimeId, updatedAt: new Date() })
|
|
273
|
-
.where(eq(tasks.id,
|
|
333
|
+
.where(eq(tasks.id, taskId));
|
|
274
334
|
|
|
275
335
|
// Fire-and-forget execution
|
|
276
336
|
const { executeTaskWithAgent } = await import("@/lib/agents/router");
|
|
277
|
-
executeTaskWithAgent(
|
|
337
|
+
executeTaskWithAgent(taskId, runtimeId).catch(() => {});
|
|
278
338
|
|
|
279
|
-
ctx.onToolResult?.("execute_task", { id:
|
|
280
|
-
return ok({ message: "Execution started", taskId
|
|
339
|
+
ctx.onToolResult?.("execute_task", { id: taskId, title: task.title });
|
|
340
|
+
return ok({ message: "Execution started", taskId, runtime: runtimeId });
|
|
281
341
|
} catch (e) {
|
|
282
342
|
return err(e instanceof Error ? e.message : "Failed to execute task");
|
|
283
343
|
}
|
|
@@ -292,17 +352,21 @@ export function taskTools(ctx: ToolContext) {
|
|
|
292
352
|
},
|
|
293
353
|
async (args) => {
|
|
294
354
|
try {
|
|
355
|
+
const resolved = await resolveEntityId(tasks, tasks.id, args.taskId);
|
|
356
|
+
if ("error" in resolved) return err(resolved.error);
|
|
357
|
+
const taskId = resolved.id;
|
|
358
|
+
|
|
295
359
|
const task = await db
|
|
296
360
|
.select()
|
|
297
361
|
.from(tasks)
|
|
298
|
-
.where(eq(tasks.id,
|
|
362
|
+
.where(eq(tasks.id, taskId))
|
|
299
363
|
.get();
|
|
300
364
|
|
|
301
|
-
if (!task) return err(`Task not found: ${
|
|
365
|
+
if (!task) return err(`Task not found: ${taskId}`);
|
|
302
366
|
if (task.status !== "running") return err(`Task is not running (status: ${task.status})`);
|
|
303
367
|
|
|
304
368
|
const { getExecution } = await import("@/lib/agents/execution-manager");
|
|
305
|
-
const execution = getExecution(
|
|
369
|
+
const execution = getExecution(taskId);
|
|
306
370
|
if (execution?.abortController) {
|
|
307
371
|
execution.abortController.abort();
|
|
308
372
|
}
|
|
@@ -310,9 +374,9 @@ export function taskTools(ctx: ToolContext) {
|
|
|
310
374
|
await db
|
|
311
375
|
.update(tasks)
|
|
312
376
|
.set({ status: "cancelled", updatedAt: new Date() })
|
|
313
|
-
.where(eq(tasks.id,
|
|
377
|
+
.where(eq(tasks.id, taskId));
|
|
314
378
|
|
|
315
|
-
return ok({ message: "Task cancelled", taskId
|
|
379
|
+
return ok({ message: "Task cancelled", taskId });
|
|
316
380
|
} catch (e) {
|
|
317
381
|
return err(e instanceof Error ? e.message : "Failed to cancel task");
|
|
318
382
|
}
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
workflowDocumentInputs,
|
|
11
11
|
} from "@/lib/db/schema";
|
|
12
12
|
import { eq, and, desc, inArray, like } from "drizzle-orm";
|
|
13
|
-
import { ok, err, type ToolContext } from "./helpers";
|
|
13
|
+
import { ok, err, resolveEntityId, type ToolContext } from "./helpers";
|
|
14
|
+
import { extractKeywords, jaccard } from "@/lib/util/similarity";
|
|
14
15
|
|
|
15
16
|
const VALID_WORKFLOW_STATUSES = [
|
|
16
17
|
"draft",
|
|
@@ -20,6 +21,107 @@ const VALID_WORKFLOW_STATUSES = [
|
|
|
20
21
|
"failed",
|
|
21
22
|
] as const;
|
|
22
23
|
|
|
24
|
+
/** Minimum Jaccard score for two workflows to count as "near duplicates". */
|
|
25
|
+
const WORKFLOW_DEDUP_THRESHOLD = 0.7;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pull the comparable text out of a workflow definition JSON string:
|
|
29
|
+
* name + each step's name + each step's prompt. Invalid JSON returns "".
|
|
30
|
+
*
|
|
31
|
+
* Shared by findSimilarWorkflows for the candidate and each existing row.
|
|
32
|
+
*/
|
|
33
|
+
function workflowComparableText(name: string, definitionJson: string | null): string {
|
|
34
|
+
const parts: string[] = [name];
|
|
35
|
+
if (!definitionJson) return parts.join(" ");
|
|
36
|
+
try {
|
|
37
|
+
const def = JSON.parse(definitionJson);
|
|
38
|
+
if (Array.isArray(def?.steps)) {
|
|
39
|
+
for (const step of def.steps) {
|
|
40
|
+
if (typeof step?.name === "string") parts.push(step.name);
|
|
41
|
+
if (typeof step?.prompt === "string") parts.push(step.prompt);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Malformed JSON — fall back to just the name.
|
|
46
|
+
}
|
|
47
|
+
return parts.join(" ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SimilarWorkflowMatch {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
similarity: number;
|
|
54
|
+
reason: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Find workflows in the same project that look similar to a candidate.
|
|
59
|
+
*
|
|
60
|
+
* Two-tier check:
|
|
61
|
+
* 1. Exact name match (case-insensitive) → similarity 1.0
|
|
62
|
+
* 2. Jaccard similarity over extracted keywords from name+step titles+prompts
|
|
63
|
+
*
|
|
64
|
+
* Returns up to 3 matches with similarity >= WORKFLOW_DEDUP_THRESHOLD,
|
|
65
|
+
* sorted by similarity descending. Used by `create_workflow` to warn the
|
|
66
|
+
* LLM before blindly inserting another row in long conversations where
|
|
67
|
+
* the sliding-window context builder evicts earlier creations.
|
|
68
|
+
*
|
|
69
|
+
* When projectId is null (no active project), returns [] — cross-project
|
|
70
|
+
* dedup would be misleading, and the handful of null-project rows that
|
|
71
|
+
* exist aren't worth de-duplicating against each other.
|
|
72
|
+
*/
|
|
73
|
+
export async function findSimilarWorkflows(
|
|
74
|
+
projectId: string | null,
|
|
75
|
+
candidateName: string,
|
|
76
|
+
candidateDefinitionJson: string
|
|
77
|
+
): Promise<SimilarWorkflowMatch[]> {
|
|
78
|
+
if (!projectId) return [];
|
|
79
|
+
|
|
80
|
+
const existing = await db
|
|
81
|
+
.select({
|
|
82
|
+
id: workflows.id,
|
|
83
|
+
name: workflows.name,
|
|
84
|
+
definition: workflows.definition,
|
|
85
|
+
})
|
|
86
|
+
.from(workflows)
|
|
87
|
+
.where(eq(workflows.projectId, projectId));
|
|
88
|
+
|
|
89
|
+
const matches: SimilarWorkflowMatch[] = [];
|
|
90
|
+
const candidateKeywords = extractKeywords(
|
|
91
|
+
workflowComparableText(candidateName, candidateDefinitionJson)
|
|
92
|
+
);
|
|
93
|
+
const candidateNameLower = candidateName.trim().toLowerCase();
|
|
94
|
+
|
|
95
|
+
for (const row of existing) {
|
|
96
|
+
// Tier 1: exact name match (case-insensitive)
|
|
97
|
+
if (row.name.trim().toLowerCase() === candidateNameLower) {
|
|
98
|
+
matches.push({
|
|
99
|
+
id: row.id,
|
|
100
|
+
name: row.name,
|
|
101
|
+
similarity: 1,
|
|
102
|
+
reason: `Same name: "${row.name}"`,
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Tier 2: Jaccard similarity on keywords
|
|
108
|
+
const existingKeywords = extractKeywords(
|
|
109
|
+
workflowComparableText(row.name, row.definition)
|
|
110
|
+
);
|
|
111
|
+
const similarity = jaccard(candidateKeywords, existingKeywords);
|
|
112
|
+
if (similarity >= WORKFLOW_DEDUP_THRESHOLD) {
|
|
113
|
+
matches.push({
|
|
114
|
+
id: row.id,
|
|
115
|
+
name: row.name,
|
|
116
|
+
similarity,
|
|
117
|
+
reason: `Similar content to "${row.name}" (${Math.round(similarity * 100)}%)`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return matches.sort((a, b) => b.similarity - a.similarity).slice(0, 3);
|
|
123
|
+
}
|
|
124
|
+
|
|
23
125
|
export function workflowTools(ctx: ToolContext) {
|
|
24
126
|
return [
|
|
25
127
|
defineTool(
|
|
@@ -69,7 +171,7 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
69
171
|
|
|
70
172
|
defineTool(
|
|
71
173
|
"create_workflow",
|
|
72
|
-
"Create a new workflow with a definition. The definition must include a pattern (sequence, parallel, checkpoint, planner-executor, swarm, loop) and steps array.",
|
|
174
|
+
"Create a new workflow with a definition. The definition must include a pattern (sequence, parallel, checkpoint, planner-executor, swarm, loop) and steps array. Sequence-pattern steps can be either task steps (with prompt + assignedAgent/agentProfile) or delay steps (with delayDuration like '3d', '2h', '30m', '1w') that pause the workflow between tasks — use delay steps for time-distributed sequences (outreach cadences, drip campaigns, cooling periods) rather than creating separate workflows or schedules. IMPORTANT: for the 'run agent on every row of a table' pattern, prefer enrich_table over create_workflow — enrich_table generates the optimal loop configuration, binds each row as {{row.field}} context, wires up the postAction row writeback, and handles idempotent skip of already-populated rows. Hand-rolled equivalents miss these safeguards.",
|
|
73
175
|
{
|
|
74
176
|
name: z.string().min(1).max(200).describe("Workflow name"),
|
|
75
177
|
projectId: z
|
|
@@ -79,7 +181,11 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
79
181
|
definition: z
|
|
80
182
|
.string()
|
|
81
183
|
.describe(
|
|
82
|
-
'Workflow definition as JSON string. Must include "pattern" and "steps" array.
|
|
184
|
+
'Workflow definition as JSON string. Must include "pattern" and "steps" array. ' +
|
|
185
|
+
'Task step example: {"id":"s1","name":"Research","prompt":"Do X","assignedAgent":"claude"}. ' +
|
|
186
|
+
'Delay step example (sequence pattern only): {"id":"s2","name":"Wait 3 days","delayDuration":"3d"}. ' +
|
|
187
|
+
'A complete drip sequence: {"pattern":"sequence","steps":[{"id":"s1","name":"Initial","prompt":"Send first email","assignedAgent":"claude"},{"id":"s2","name":"Wait","delayDuration":"3d"},{"id":"s3","name":"Follow-up","prompt":"Send follow-up","assignedAgent":"claude"}]}. ' +
|
|
188
|
+
'Delay bounds: 1m to 30d. Delay steps must NOT have prompt/profile fields.'
|
|
83
189
|
),
|
|
84
190
|
documentIds: z
|
|
85
191
|
.array(z.string())
|
|
@@ -93,6 +199,12 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
93
199
|
.describe(
|
|
94
200
|
"Runtime to use for workflow execution (e.g., 'openai-direct', 'anthropic-direct'). Use list_runtimes to see available options. Omit to use the system default."
|
|
95
201
|
),
|
|
202
|
+
force: z
|
|
203
|
+
.boolean()
|
|
204
|
+
.optional()
|
|
205
|
+
.describe(
|
|
206
|
+
"Set to true to bypass the near-duplicate check and always create a new workflow. Only use this when the user has explicitly confirmed they want a second workflow alongside a similar existing one (e.g., 'v2', 'alternate approach'). Default false."
|
|
207
|
+
),
|
|
96
208
|
},
|
|
97
209
|
async (args) => {
|
|
98
210
|
try {
|
|
@@ -127,6 +239,28 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
127
239
|
}
|
|
128
240
|
|
|
129
241
|
const effectiveProjectId = args.projectId ?? ctx.projectId ?? null;
|
|
242
|
+
|
|
243
|
+
// Dedup guard: long chat conversations can truncate the earlier
|
|
244
|
+
// create_workflow tool call out of the sliding-window context, so
|
|
245
|
+
// the LLM loses its own history and re-creates on "redesign"
|
|
246
|
+
// requests. Check for near-duplicates in the same project before
|
|
247
|
+
// inserting. Pass force=true to bypass.
|
|
248
|
+
if (!args.force) {
|
|
249
|
+
const similar = await findSimilarWorkflows(
|
|
250
|
+
effectiveProjectId,
|
|
251
|
+
args.name,
|
|
252
|
+
args.definition
|
|
253
|
+
);
|
|
254
|
+
if (similar.length > 0) {
|
|
255
|
+
return ok({
|
|
256
|
+
status: "similar-found",
|
|
257
|
+
message:
|
|
258
|
+
"Found similar workflow(s) in this project. Use update_workflow to modify an existing one, or pass force=true to create a new workflow alongside them.",
|
|
259
|
+
matches: similar,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
130
264
|
const now = new Date();
|
|
131
265
|
const id = crypto.randomUUID();
|
|
132
266
|
|
|
@@ -211,13 +345,17 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
211
345
|
},
|
|
212
346
|
async (args) => {
|
|
213
347
|
try {
|
|
214
|
-
const
|
|
348
|
+
const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
|
|
349
|
+
if ("error" in resolved) return err(resolved.error);
|
|
350
|
+
const workflowId = resolved.id;
|
|
351
|
+
|
|
352
|
+
const workflow = db
|
|
215
353
|
.select()
|
|
216
354
|
.from(workflows)
|
|
217
|
-
.where(eq(workflows.id,
|
|
355
|
+
.where(eq(workflows.id, workflowId))
|
|
218
356
|
.get();
|
|
219
357
|
|
|
220
|
-
if (!workflow) return err(`Workflow not found: ${
|
|
358
|
+
if (!workflow) return err(`Workflow not found: ${workflowId}`);
|
|
221
359
|
|
|
222
360
|
const { parseWorkflowState } = await import("@/lib/workflows/engine");
|
|
223
361
|
const { definition, state } = parseWorkflowState(workflow.definition);
|
|
@@ -266,13 +404,17 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
266
404
|
},
|
|
267
405
|
async (args) => {
|
|
268
406
|
try {
|
|
269
|
-
const
|
|
407
|
+
const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
|
|
408
|
+
if ("error" in resolved) return err(resolved.error);
|
|
409
|
+
const workflowId = resolved.id;
|
|
410
|
+
|
|
411
|
+
const existing = db
|
|
270
412
|
.select()
|
|
271
413
|
.from(workflows)
|
|
272
|
-
.where(eq(workflows.id,
|
|
414
|
+
.where(eq(workflows.id, workflowId))
|
|
273
415
|
.get();
|
|
274
416
|
|
|
275
|
-
if (!existing) return err(`Workflow not found: ${
|
|
417
|
+
if (!existing) return err(`Workflow not found: ${workflowId}`);
|
|
276
418
|
if (existing.status !== "draft")
|
|
277
419
|
return err(`Cannot edit a workflow in '${existing.status}' status. Only draft workflows can be edited.`);
|
|
278
420
|
|
|
@@ -293,12 +435,12 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
293
435
|
await db
|
|
294
436
|
.update(workflows)
|
|
295
437
|
.set(updates)
|
|
296
|
-
.where(eq(workflows.id,
|
|
438
|
+
.where(eq(workflows.id, workflowId));
|
|
297
439
|
|
|
298
440
|
const [workflow] = await db
|
|
299
441
|
.select()
|
|
300
442
|
.from(workflows)
|
|
301
|
-
.where(eq(workflows.id,
|
|
443
|
+
.where(eq(workflows.id, workflowId));
|
|
302
444
|
|
|
303
445
|
ctx.onToolResult?.("update_workflow", workflow);
|
|
304
446
|
return ok({
|
|
@@ -321,13 +463,17 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
321
463
|
},
|
|
322
464
|
async (args) => {
|
|
323
465
|
try {
|
|
324
|
-
const
|
|
466
|
+
const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
|
|
467
|
+
if ("error" in resolved) return err(resolved.error);
|
|
468
|
+
const workflowId = resolved.id;
|
|
469
|
+
|
|
470
|
+
const existing = db
|
|
325
471
|
.select()
|
|
326
472
|
.from(workflows)
|
|
327
|
-
.where(eq(workflows.id,
|
|
473
|
+
.where(eq(workflows.id, workflowId))
|
|
328
474
|
.get();
|
|
329
475
|
|
|
330
|
-
if (!existing) return err(`Workflow not found: ${
|
|
476
|
+
if (!existing) return err(`Workflow not found: ${workflowId}`);
|
|
331
477
|
if (existing.status === "active")
|
|
332
478
|
return err("Cannot delete an active workflow. Pause or stop it first.");
|
|
333
479
|
|
|
@@ -335,7 +481,7 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
335
481
|
const childTasks = await db
|
|
336
482
|
.select({ id: tasks.id })
|
|
337
483
|
.from(tasks)
|
|
338
|
-
.where(eq(tasks.workflowId,
|
|
484
|
+
.where(eq(tasks.workflowId, workflowId));
|
|
339
485
|
|
|
340
486
|
const taskIds = childTasks.map((t) => t.id);
|
|
341
487
|
for (const taskId of taskIds) {
|
|
@@ -343,10 +489,10 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
343
489
|
await db.delete(agentLogs).where(eq(agentLogs.taskId, taskId));
|
|
344
490
|
await db.delete(documents).where(eq(documents.taskId, taskId));
|
|
345
491
|
}
|
|
346
|
-
await db.delete(tasks).where(eq(tasks.workflowId,
|
|
347
|
-
await db.delete(workflows).where(eq(workflows.id,
|
|
492
|
+
await db.delete(tasks).where(eq(tasks.workflowId, workflowId));
|
|
493
|
+
await db.delete(workflows).where(eq(workflows.id, workflowId));
|
|
348
494
|
|
|
349
|
-
return ok({ message: "Workflow deleted", workflowId
|
|
495
|
+
return ok({ message: "Workflow deleted", workflowId, name: existing.name });
|
|
350
496
|
} catch (e) {
|
|
351
497
|
return err(e instanceof Error ? e.message : "Failed to delete workflow");
|
|
352
498
|
}
|
|
@@ -361,13 +507,17 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
361
507
|
},
|
|
362
508
|
async (args) => {
|
|
363
509
|
try {
|
|
364
|
-
const
|
|
510
|
+
const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
|
|
511
|
+
if ("error" in resolved) return err(resolved.error);
|
|
512
|
+
const workflowId = resolved.id;
|
|
513
|
+
|
|
514
|
+
const workflow = db
|
|
365
515
|
.select()
|
|
366
516
|
.from(workflows)
|
|
367
|
-
.where(eq(workflows.id,
|
|
517
|
+
.where(eq(workflows.id, workflowId))
|
|
368
518
|
.get();
|
|
369
519
|
|
|
370
|
-
if (!workflow) return err(`Workflow not found: ${
|
|
520
|
+
if (!workflow) return err(`Workflow not found: ${workflowId}`);
|
|
371
521
|
|
|
372
522
|
// Allow re-execution from crashed "active" if no live tasks
|
|
373
523
|
if (workflow.status === "active") {
|
|
@@ -376,7 +526,7 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
376
526
|
.from(tasks)
|
|
377
527
|
.where(
|
|
378
528
|
and(
|
|
379
|
-
eq(tasks.workflowId,
|
|
529
|
+
eq(tasks.workflowId, workflowId),
|
|
380
530
|
inArray(tasks.status, ["running", "queued"])
|
|
381
531
|
)
|
|
382
532
|
);
|
|
@@ -404,7 +554,7 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
404
554
|
.set({ status: "cancelled", updatedAt: new Date() })
|
|
405
555
|
.where(
|
|
406
556
|
and(
|
|
407
|
-
eq(tasks.workflowId,
|
|
557
|
+
eq(tasks.workflowId, workflowId),
|
|
408
558
|
inArray(tasks.status, ["running", "queued"])
|
|
409
559
|
)
|
|
410
560
|
);
|
|
@@ -419,27 +569,71 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
419
569
|
status: "draft",
|
|
420
570
|
updatedAt: new Date(),
|
|
421
571
|
})
|
|
422
|
-
.where(eq(workflows.id,
|
|
572
|
+
.where(eq(workflows.id, workflowId));
|
|
423
573
|
}
|
|
424
574
|
|
|
425
575
|
// Atomic claim: set to active
|
|
426
576
|
await db
|
|
427
577
|
.update(workflows)
|
|
428
578
|
.set({ status: "active", updatedAt: new Date() })
|
|
429
|
-
.where(eq(workflows.id,
|
|
579
|
+
.where(eq(workflows.id, workflowId));
|
|
430
580
|
|
|
431
581
|
// Fire-and-forget
|
|
432
582
|
const { executeWorkflow } = await import("@/lib/workflows/engine");
|
|
433
|
-
executeWorkflow(
|
|
583
|
+
executeWorkflow(workflowId).catch(() => {});
|
|
434
584
|
|
|
435
|
-
ctx.onToolResult?.("execute_workflow", { id:
|
|
436
|
-
return ok({ message: "Workflow execution started", workflowId
|
|
585
|
+
ctx.onToolResult?.("execute_workflow", { id: workflowId, name: workflow.name });
|
|
586
|
+
return ok({ message: "Workflow execution started", workflowId, name: workflow.name });
|
|
437
587
|
} catch (e) {
|
|
438
588
|
return err(e instanceof Error ? e.message : "Failed to execute workflow");
|
|
439
589
|
}
|
|
440
590
|
}
|
|
441
591
|
),
|
|
442
592
|
|
|
593
|
+
defineTool(
|
|
594
|
+
"resume_workflow",
|
|
595
|
+
"Resume a workflow that is paused at a delay step, immediately skipping the remaining delay. Use when the user says 'resume now' or 'skip the wait' for a paused workflow. Only works if the workflow status is 'paused' — a 409 response means the scheduler already resumed it. Requires approval.",
|
|
596
|
+
{
|
|
597
|
+
workflowId: z.string().describe("The workflow ID to resume"),
|
|
598
|
+
},
|
|
599
|
+
async (args) => {
|
|
600
|
+
try {
|
|
601
|
+
const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
|
|
602
|
+
if ("error" in resolved) return err(resolved.error);
|
|
603
|
+
const workflowId = resolved.id;
|
|
604
|
+
|
|
605
|
+
const workflow = db
|
|
606
|
+
.select()
|
|
607
|
+
.from(workflows)
|
|
608
|
+
.where(eq(workflows.id, workflowId))
|
|
609
|
+
.get();
|
|
610
|
+
|
|
611
|
+
if (!workflow) return err(`Workflow not found: ${workflowId}`);
|
|
612
|
+
|
|
613
|
+
if (workflow.status !== "paused") {
|
|
614
|
+
return err(
|
|
615
|
+
`Workflow is not paused (current status: ${workflow.status}). Only paused workflows can be resumed.`,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const { resumeWorkflow } = await import("@/lib/workflows/engine");
|
|
620
|
+
// Fire-and-forget: resumeWorkflow performs atomic status transition internally.
|
|
621
|
+
resumeWorkflow(workflowId).catch((error) => {
|
|
622
|
+
console.error(`Workflow ${workflowId} resume failed:`, error);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
ctx.onToolResult?.("resume_workflow", { id: workflowId, name: workflow.name });
|
|
626
|
+
return ok({
|
|
627
|
+
message: "Workflow resume dispatched",
|
|
628
|
+
workflowId,
|
|
629
|
+
name: workflow.name,
|
|
630
|
+
});
|
|
631
|
+
} catch (e) {
|
|
632
|
+
return err(e instanceof Error ? e.message : "Failed to resume workflow");
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
),
|
|
636
|
+
|
|
443
637
|
defineTool(
|
|
444
638
|
"get_workflow_status",
|
|
445
639
|
"Get the current execution status of a workflow, including step-by-step progress.",
|
|
@@ -448,13 +642,17 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
448
642
|
},
|
|
449
643
|
async (args) => {
|
|
450
644
|
try {
|
|
451
|
-
const
|
|
645
|
+
const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
|
|
646
|
+
if ("error" in resolved) return err(resolved.error);
|
|
647
|
+
const workflowId = resolved.id;
|
|
648
|
+
|
|
649
|
+
const workflow = db
|
|
452
650
|
.select()
|
|
453
651
|
.from(workflows)
|
|
454
|
-
.where(eq(workflows.id,
|
|
652
|
+
.where(eq(workflows.id, workflowId))
|
|
455
653
|
.get();
|
|
456
654
|
|
|
457
|
-
if (!workflow) return err(`Workflow not found: ${
|
|
655
|
+
if (!workflow) return err(`Workflow not found: ${workflowId}`);
|
|
458
656
|
|
|
459
657
|
const { parseWorkflowState } = await import("@/lib/workflows/engine");
|
|
460
658
|
const { definition, state } = parseWorkflowState(workflow.definition);
|
|
@@ -531,11 +729,15 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
531
729
|
}
|
|
532
730
|
|
|
533
731
|
if (args.sourceWorkflowId) {
|
|
732
|
+
const resolvedSrc = await resolveEntityId(workflows, workflows.id, args.sourceWorkflowId);
|
|
733
|
+
if ("error" in resolvedSrc) return err(resolvedSrc.error);
|
|
734
|
+
const srcWorkflowId = resolvedSrc.id;
|
|
735
|
+
|
|
534
736
|
// Find task IDs belonging to the source workflow
|
|
535
737
|
const workflowTasks = await db
|
|
536
738
|
.select({ id: tasks.id })
|
|
537
739
|
.from(tasks)
|
|
538
|
-
.where(eq(tasks.workflowId,
|
|
740
|
+
.where(eq(tasks.workflowId, srcWorkflowId));
|
|
539
741
|
|
|
540
742
|
const taskIds = workflowTasks.map((t) => t.id);
|
|
541
743
|
if (taskIds.length > 0) {
|