-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat: delegate task #3549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: delegate task #3549
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import type { DelegateTaskResult } from "@t3tools/contracts"; | ||
| import * as Effect from "effect/Effect"; | ||
|
|
||
| import { TaskOrchestrator } from "../../../orchestration/Services/TaskOrchestrator.ts"; | ||
| import * as McpInvocationContext from "../../McpInvocationContext.ts"; | ||
| import { DelegationToolkit } from "./tools.ts"; | ||
|
|
||
| // Bounded wait kept comfortably under typical MCP client request timeouts so a | ||
| // single tool call never times out, even when sub-tasks take minutes. Slow | ||
| // sub-tasks come back 'running' and are picked up by collect_delegated_tasks. | ||
| const INLINE_WAIT_MS = 30_000; | ||
|
|
||
| const handlers = { | ||
| delegate_tasks: (input) => | ||
| Effect.gen(function* () { | ||
| const scope = yield* McpInvocationContext.McpInvocationContext; | ||
| const orchestrator = yield* TaskOrchestrator; | ||
| // Depth-1 recursion guard: a thread that was itself spawned as a | ||
| // delegated sub-task may not delegate further. | ||
| const isChild = yield* orchestrator.isDelegatedChild(scope.threadId); | ||
| if (isChild) { | ||
| return yield* Effect.fail({ | ||
| reason: "Delegated sub-tasks cannot delegate further (depth-1 limit).", | ||
| }); | ||
| } | ||
| const started = yield* orchestrator.startTasks({ | ||
| parentThreadId: scope.threadId, | ||
| tasks: input.tasks, | ||
| maxConcurrency: input.maxConcurrency, | ||
| }); | ||
| const runningIds = started.results | ||
| .filter((result) => result.status === "running") | ||
| .map((result) => result.threadId); | ||
| if (runningIds.length === 0) { | ||
| return started; | ||
| } | ||
| // Wait a bounded time so fast sub-tasks come back inline; the rest stay | ||
| // 'running' for collect_delegated_tasks to pick up later. | ||
| const collected = yield* orchestrator.collectTasks({ | ||
| parentThreadId: scope.threadId, | ||
| threadIds: runningIds, | ||
| waitMs: INLINE_WAIT_MS, | ||
| }); | ||
| const collectedByThread = new Map<string, DelegateTaskResult>( | ||
| collected.results.map((result) => [result.threadId, result]), | ||
| ); | ||
| const results = started.results.map((result) => | ||
| result.status === "error" ? result : (collectedByThread.get(result.threadId) ?? result), | ||
| ); | ||
| return { results }; | ||
| }), | ||
| collect_delegated_tasks: (input) => | ||
| Effect.gen(function* () { | ||
| const scope = yield* McpInvocationContext.McpInvocationContext; | ||
| const orchestrator = yield* TaskOrchestrator; | ||
| return yield* orchestrator.collectTasks({ | ||
| parentThreadId: scope.threadId, | ||
| threadIds: input.threadIds, | ||
| waitMs: INLINE_WAIT_MS, | ||
| }); | ||
| }), | ||
|
Comment on lines
+52
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High
- return yield* orchestrator.collectTasks({
- parentThreadId: scope.threadId,
- threadIds: input.threadIds,
- waitMs: INLINE_WAIT_MS,
- });
+ // Omit threadIds to let orchestrator return only this thread's delegated children
+ return yield* orchestrator.collectTasks({
+ parentThreadId: scope.threadId,
+ waitMs: INLINE_WAIT_MS,
+ });🤖 Copy this AI Prompt to have your agent fix this: |
||
| } satisfies Parameters<typeof DelegationToolkit.toLayer>[0]; | ||
|
|
||
| export const DelegationToolkitHandlersLive = DelegationToolkit.toLayer(handlers); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { | ||
| CollectDelegatedTasksRequest, | ||
| DelegateTasksRequest, | ||
| DelegateTasksResult, | ||
| } from "@t3tools/contracts"; | ||
| import * as Schema from "effect/Schema"; | ||
| import { Tool, Toolkit } from "effect/unstable/ai"; | ||
|
|
||
| import { TaskOrchestrator } from "../../../orchestration/Services/TaskOrchestrator.ts"; | ||
| import * as McpInvocationContext from "../../McpInvocationContext.ts"; | ||
|
|
||
| const dependencies = [McpInvocationContext.McpInvocationContext, TaskOrchestrator]; | ||
|
|
||
| /** Returned only when the call is refused (e.g. a child trying to delegate). */ | ||
| export const DelegateTasksFailure = Schema.Struct({ | ||
| reason: Schema.String, | ||
| }); | ||
|
|
||
| export const DelegateTasksTool = Tool.make("delegate_tasks", { | ||
| description: | ||
| "Delegate sub-tasks to fresh child agents that run in parallel. Returns quickly: each result is " + | ||
| "either finished ('completed'/'error') or still 'running' (with its `threadId`). For any result " + | ||
| "with status 'running', call `collect_delegated_tasks` afterwards to get its final message — do not " + | ||
| "assume a 'running' task failed. " + | ||
| "WHEN TO USE — proactively, without being asked: whenever a request breaks into 2+ parts that do " + | ||
| "not depend on each other's output (research several files at once, answer multiple questions, " + | ||
| "review/summarize different modules, independent edits). Skip it for a single linear task or when " + | ||
| "one step's result feeds the next. " + | ||
| "CHOOSING THE MODEL per sub-task — set `modelHint`: 'cheap' for trivial/mechanical lookups (list " + | ||
| "files, grep, count, simple summaries), 'balanced' for normal work (default when unsure), 'strong' " + | ||
| "for hard reasoning, code review, or tricky debugging. For planning or design sub-tasks, put the word " + | ||
| '"plan" in the `label` so they route to the strongest model. Pass `modelSelection` instead only when ' + | ||
| "the user named a specific model. " + | ||
| "Always give each sub-task a short descriptive `label`. Sub-tasks share this thread's working " + | ||
| "directory and run concurrently, so keep their file changes non-overlapping to avoid conflicts.", | ||
| parameters: DelegateTasksRequest, | ||
| success: DelegateTasksResult, | ||
| failure: DelegateTasksFailure, | ||
| dependencies, | ||
| }) | ||
| .annotate(Tool.Title, "Delegate sub-tasks") | ||
| .annotate(Tool.Destructive, true) | ||
| .annotate(Tool.OpenWorld, true); | ||
|
|
||
| export const CollectDelegatedTasksTool = Tool.make("collect_delegated_tasks", { | ||
| description: | ||
| "Collect results for sub-tasks that were still 'running' when `delegate_tasks` (or a prior " + | ||
| "`collect_delegated_tasks`) returned. Pass the `threadIds` of the running sub-tasks, or omit them to " + | ||
| "collect every still-running sub-task you delegated from this thread. Returns quickly; any task still " + | ||
| "not finished comes back as 'running' again — keep calling until none remain 'running'.", | ||
| parameters: CollectDelegatedTasksRequest, | ||
| success: DelegateTasksResult, | ||
| dependencies, | ||
| }) | ||
| .annotate(Tool.Title, "Collect delegated sub-tasks") | ||
| .annotate(Tool.Readonly, true) | ||
| .annotate(Tool.Idempotent, true); | ||
|
|
||
| export const DelegationToolkit = Toolkit.make(DelegateTasksTool, CollectDelegatedTasksTool); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated Baha desktop branding
High Severity
The change to
productNamerenames the desktop app to "T3 Code Baha" and implicitly changes its bundle ID. This alters production packaging, app updates, and macOS passkey entitlements, which seems outside the PR's stated scope.Reviewed by Cursor Bugbot for commit c5e06ec. Configure here.