diff --git a/PRODUCT.md b/PRODUCT.md index 3858fe725..7992d3b7f 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -16,8 +16,7 @@ conversation; the conversation is the unit of work. This is why the sidebar is one list of workbenches, not sections split by kind — every workbench a person has, agent conversation or group -conversation alike, shows up the same way — plus an in-progress -**Working** strip when the signed-in user has tasks still running. See +conversation alike, shows up the same way. See [docs/GLOSSARY.md](docs/GLOSSARY.md) for the full term mapping and [docs/CHAT.md](docs/CHAT.md) for how a conversation is built underneath. @@ -67,7 +66,7 @@ conversation rather than an empty shell: conversation, and its first reply introduces itself and names what it can do. There is no separate screen for this step: the drafted system prompt itself carries the instruction (see - `packages/task-planner/src/agent-definition-drafting.ts`), so the + `packages/agent-directory/src/agent-definition-drafting.ts`), so the greeting arrives as an ordinary streamed reply in the new workbench. A bench that already has one or more workbenches skips straight past step @@ -113,9 +112,9 @@ again, on what schedule, and where the result should land. See Two account-wide surfaces sit outside any single workbench: - **Inbox** projects a person's mail into three groups — action, mention, - delivery — with mark-all-read and clear-done bulk actions. It is where a - task's result, a mention, or a routine's delivery lands once the - triggering activity is done. See `packages/inbox`. + delivery — with mark-all-read and clear-done bulk actions. It is where + a mention or a routine's delivery lands once the triggering activity is + done. See `packages/inbox`. - **Approvals ("needs you")** surface a paused agent run waiting on a human decision. There is no dedicated Approvals page — pending approvals show up in the Activity band, a permanent section of the contextual diff --git a/apps/hub/package.json b/apps/hub/package.json index 70299ce07..9dad84cad 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -26,6 +26,7 @@ "@corbits/docker-provisioner": "workspace:*", "@corbits/e2b-sandbox-sidecar": "workspace:*", "@corbits/evals": "workspace:*", + "@corbits/folded-run-one-shot": "workspace:*", "@corbits/folded-runs": "workspace:*", "@corbits/inbox": "workspace:*", "@corbits/inference-catalog": "workspace:*", @@ -44,8 +45,6 @@ "@corbits/agent-workflow-authoring": "workspace:*", "@corbits/skills": "workspace:*", "@corbits/slack-tag": "workspace:*", - "@corbits/task-planner": "workspace:*", - "@corbits/tasks": "workspace:*", "@corbits/tool-registry-publish": "workspace:*", "@corbits/turn-artifacts": "workspace:*", "@corbits/workflow-deploy-source": "workspace:*", diff --git a/apps/hub/src/index.ts b/apps/hub/src/index.ts index d9c3cce82..40540c07f 100644 --- a/apps/hub/src/index.ts +++ b/apps/hub/src/index.ts @@ -32,10 +32,10 @@ import { createEnvKeyCredentialCipher, createNoopCredentialCipher, } from "@intx/crypto"; -import { authorize, timeWindowEvaluator } from "@intx/authz"; +import { timeWindowEvaluator } from "@intx/authz"; import type { ConditionRegistry } from "@intx/types/authz"; import { credentialAad } from "@intx/types"; -import type { CredentialBinding, CredentialCipher } from "@intx/types"; +import type { CredentialCipher } from "@intx/types"; import { createApp, createMailTriggeredRunGrantsMaterializer, @@ -46,17 +46,23 @@ import { } from "@intx/hub-api"; import { - agentDefinitionSourceTree, - buildAgentDefinitionWorkflow, createAgentDefinitionRoutes, createDefinitionAssetHistory, createDrizzleDefinitionSkillsStore, createWorkflowAgentCreateRoutes, createWorkflowCapabilityRoutes, createWorkflowSkillPinRoutes, - reindexPinnedSkills, - serializeAgentDefinitionWorkflow, type CapabilityInventoryProvider, + AgentDefinitionDraftReferenceOutOfInventoryError, + AgentDefinitionDraftReplyUnparseableError, + createMyraAgentDefinitionDrafting, + isPlannerCreatedDefinitionName, + MyraAgentDefinitionDraftingUnavailableError, + resolveMyraDefinitionIdFromDb, + type InventoryAgent, + type InventoryModel, + type InventorySources, + type InventoryToolPackage, } from "@corbits/agent-directory"; import { @@ -84,7 +90,6 @@ import { createHubChatPlatform, createNoopInferenceRoutes, createRelaunchNoticePoster, - findExistingAgentChat, createWorkflowParticipantRoutes, isWorkbenchHostDefinitionName, listConnectedProviders, @@ -95,7 +100,6 @@ import { workbenchLaunchPersistExtra, } from "@corbits/chat"; import type { RelaunchNoticePort } from "@corbits/chat"; -import { reportError } from "@corbits/error-sink"; import type { FinalizedTurnToolCall } from "@corbits/turn-artifacts"; import { decodedOrNull } from "@corbits/url-path"; import { @@ -187,15 +191,6 @@ import { routineRun as routineRunTable, type RoutineDraftInventoryWorkflow, } from "@corbits/routines"; -import { createAgentLifecycle } from "@corbits/agent-lifecycle"; -import { - createDrizzleTaskStore, - createStuckLegSweep, - createTaskOrchestrator, - createTaskRoutes, - launchTask, - launchTaskLeg, -} from "@corbits/tasks"; import { createSidecarProvisioner as createE2BSidecarProvisioner, readProvisionerConfig as readE2BProvisionerConfig, @@ -211,19 +206,10 @@ import { withDeploySourceRecording, } from "@corbits/workflow-deploy-source"; import { - createMyraAgentDefinitionDrafting, - createPlannerRoutes, - createWorkflowDispatchRoutes, - dispatchWithPlanner, - isPlannerCreatedDefinitionName, - PlannerDefinitionGrantDeniedError, - resolveMyraDefinitionIdFromDb, + FoldedRunFailedError, + FoldedRunTimedOutError, runOneShotFoldedPrompt, - type InventoryAgent, - type InventoryModel, - type InventorySources, - type InventoryToolPackage, -} from "@corbits/task-planner"; +} from "@corbits/folded-run-one-shot"; import { createEventCollectorRegistry, @@ -404,15 +390,6 @@ const SignInEmailBody = type({ email: "string" }); // emergency bump (8ca85543) band-aided around was genuinely lossy: a // later `wakeByAddress` relaunch resumes the same run rather than // starting a fresh one. -// -// `TASK_IDLE_UNDEPLOY_MS` remains for `taskLifecycle` below and stays a -// genuinely different, longer threshold: a spawn-and-return task's -// `wake` is unreachable by construction (a one-shot task never sends a -// follow-up after its opening prompt), so there is nothing a relaunch -// could ever revive — undeploying an idle task-launched resident is -// exactly the intended cleanup once the task's single turn has settled, -// not a case this state-preserving reap needs to cover. -const TASK_IDLE_UNDEPLOY_MS = 8 * 60 * 60 * 1000; // Signup mode is operator-controlled (WORKBENCH_SIGNUP). Default closed: // self-serve email signup is rejected; owners add users or share a @@ -644,7 +621,7 @@ export async function createHub(config: HubConfig) { // for the process, read here ahead of `workflow_run` and written to // there off every `agent.deploy.ack`. const runKeyHistoryStore = createDrizzleRunKeyHistoryStore(db); - // A folded run (a workbench host, an invited agent, a task) settles + // A folded run (a workbench host, an invited agent) settles // "completed" between message occurrences as part of its own normal // wake/redeploy cycle — not "done forever" the way a one-shot // workflow deployment's "completed" is. The platform's own @@ -1315,11 +1292,9 @@ export async function createHub(config: HubConfig) { mcpCredentialBindingsFor, // Chat residents are undeployed on idle again (see the comment above // this function): `chatIdleReapMs` (env-overridable via - // `WORKBENCH_CHAT_IDLE_REAP_MS`, default 30 minutes) is a genuinely - // separate threshold from `TASK_IDLE_UNDEPLOY_MS` — chat's reap is - // state-preserving (`IDLE_HIBERNATE_UNDEPLOY_REASON`) so a much - // shorter window is safe here, unlike the destructive undeploy a - // task-launched resident gets. + // `WORKBENCH_CHAT_IDLE_REAP_MS`, default 30 minutes) is + // state-preserving (`IDLE_HIBERNATE_UNDEPLOY_REASON`), unlike a + // destructive undeploy. lifecycle: { idleSleepMs: config.chatIdleReapMs }, // // A hand-authored definition with no model requirements of its own @@ -1488,30 +1463,24 @@ export async function createHub(config: HubConfig) { ); // The one "is this a conversational agent?" ruling, shared by every - // picker that offers agents to a person AND every taskability gate — - // chat's invite/new-chat pickers, the task composer, and task-planner's - // {use} target validation alike: automatable catalog workflows + // picker that offers agents to a person and by a routine's `"agent"`-kind + // trigger-field validation below: automatable catalog workflows // (routines material) and workbench-host anchor definitions (chat's own // plumbing, never a person-facing agent) belong in neither. const isConversationalAgentDefinition = (definition: { name: string }) => !isAutomatableWorkflowName(definition.name) && !isWorkbenchHostDefinitionName(definition.name); - // A second, narrower ruling layered on top of the taskability gate - // above, for LISTING/PICKER surfaces only — never for taskability - // itself. A planner-created agent (CL-6051's `{create}` branch, see - // `@corbits/task-planner`'s `planner-created-naming.ts`) exists for - // exactly one task; it must stay fully launchable (`spawnFromTaskSpec` - // calls `launchTask` against the definition it just created) but must - // never clutter a picker meant for agents a person deliberately keeps - // around. Wired into every picker surface: chat's invite/new-chat - // dialogs (`chatDeps.isInvitableDefinition` below) and the planner's - // own inventory (`listMyraConversationalAgents` below) — the manual - // task composer's picker (`apps/web`'s - // `listTenantInvitableDefinitions`) calls the same chat route, so it - // inherits this for free. `@corbits/agent-directory` has no listing - // route of its own today (only create/read-skills/update-skills), so - // there is no third surface to thread this through there yet. + // A second, narrower ruling layered on top of the ruling above, for + // LISTING/PICKER surfaces only. A planner-created agent (the + // now-deleted tasks primitive's planner `{create}` branch, CL-6051; + // see `@corbits/agent-directory`'s `stale-task-agent-naming.ts`) + // existed for exactly one now-retired task; any that still linger + // must stay out of a picker meant for agents a person deliberately + // keeps around. Wired into every picker surface: + // chat's invite/new-chat dialogs (`chatDeps.isInvitableDefinition` below) + // and the agent-definition drafting inventory + // (`listMyraConversationalAgents` below). const isPickerListableDefinition = (definition: { name: string }) => isConversationalAgentDefinition(definition) && !isPlannerCreatedDefinitionName(definition.name); @@ -1828,12 +1797,11 @@ export async function createHub(config: HubConfig) { }), ); // The guided-capability-add fail-closed check reuses the exact same - // listers `plannerInventorySources` (below) wires — `@corbits/agent-directory` - // cannot import `@corbits/task-planner`'s `InventorySources`/`PlannerInventory` - // types directly (task-planner already depends on agent-directory, so that - // edge would cycle), but the tenant's live inventory of usable tool - // packages, skills, and models is never assembled twice: both this - // provider and the planner's own inventory read through the same + // listers `plannerInventorySources` (below) wires — both this provider + // and the drafting inventory read `InventorySources`/`PlannerInventory` + // from `@corbits/agent-directory` directly, and the tenant's live + // inventory of usable tool packages, skills, and models is never + // assembled twice: both share the same // `listMyraUsableToolPackages`/`listMyraModels`/`skills.registry.list` // functions (declared further down this file, hoisted). const capabilityInventory: CapabilityInventoryProvider = { @@ -2279,10 +2247,9 @@ export async function createHub(config: HubConfig) { ); // Template block workflows (CL-6405): the instantiate path's // `deployBlockWorkflow` port lands here — the same source-form - // materialization `deployAgentDefinition` below runs for a - // participant agent (asset + `@corbits/workflow-source` tree + - // `freezeInertWorkflowDefinition`), applied to a template's - // referenced block definition (`code-review` today). + // materialization pattern (asset + `@corbits/workflow-source` tree + + // `freezeInertWorkflowDefinition`) applied to a template's referenced + // block definition (`code-review` today). app.route( `${TENANT_PREFIX}/template-blocks`, createTemplateBlockRoutes({ @@ -2473,137 +2440,29 @@ export async function createHub(config: HubConfig) { }, }); - // Spawn-and-return agent tasks (`@corbits/tasks`, CL-6049): a prompt - // plus an agent definition launches a one-shot folded run with no - // workbench, and its finalized reply lands in the Inbox through the - // same notify delivery adapter `credentialExpirySweep` uses above. - // Own idle-sleep-and-UNDEPLOY lifecycle instance (the same - // `@corbits/agent-lifecycle` package chat's platform adapter used to - // drive before CL-5477 retired that binding) — kept here, unlike - // chat's, because `wake` is never actually called: a task's run only - // ever needs waking to deliver a follow-up message, and a one-shot task - // never sends one after its opening prompt. There is no run left to - // lose by undeploying once one goes idle, so this sweep's undeploy is - // genuine cleanup, not the lossy "kill what the wake path cannot - // revive" failure mode chat's own lifecycle had. - const taskStore = createDrizzleTaskStore(db); - const taskLifecycle = createAgentLifecycle({ - idleSleepMs: TASK_IDLE_UNDEPLOY_MS, - isRoutable: (address) => - sidecarRouter.getRoutableAddresses().includes(address), - undeploy: (address, reason) => - sidecarRouter.sendAgentUndeploy(address, reason), - wake: () => { - throw new Error( - "a task-launched run is never woken after its opening prompt", - ); - }, - isBusy: (address) => - typeof eventCollectors.getCurrentTurnId(address) === "string", - log: getLogger(["tasks", "lifecycle"]), - }); - const taskNotifyDeps = { - mail: mailboxDelivery, - addressing: { - inbox: (recipient: { principalId: string }) => - `${recipient.principalId}@inbox.${notifyHost}`, - from: (kind: string) => `${kind}@notify.${notifyHost}`, - }, - dispatch: createInMemoryNotifyDispatchStore(), - sinks: createSinkRegistry(), - }; - const taskLauncherDeps = { - db, - store: taskStore, - foldedRuns: { - db, - sessionService, - assetService, - sidecarRouter, - eventCollectors, - credentialCipher, - hubPublicKey, - toolGrantsForPins, - mcpCredentialBindingsFor, - }, - cryptoProviders: createCryptoProviderCache(), - notify: taskNotifyDeps, - isTaskableDefinition: isConversationalAgentDefinition, - lifecycle: taskLifecycle, - }; - const taskOrchestrator = createTaskOrchestrator({ + // Shared `FoldedRunsDeps` for every one-shot Myra prompt below (routine + // drafting, agent-definition drafting): a real one-shot inference call + // that launches a folded run, awaits its single reply, and tears the run + // down immediately — never a resident that outlives the request, so no + // idle-sleep lifecycle is needed for it. + const oneShotFoldedRunsDeps = { db, - store: taskStore, - events: sidecarRouter.events, - notify: taskNotifyDeps, - recordActivity: (address) => taskLifecycle.recordActivity(address), - launchLeg: (input) => launchTaskLeg(taskLauncherDeps, input), - workbench: { - async post({ tenantId, workbenchId, text }) { - await chatPlatform.sendMail({ - tenantId, - workbenchId, - fromWorkbenchId: workbenchId, - content: { content: text }, - }); - }, - async resolveFallbackWorkbenchId(tenantId) { - try { - const myraDefinitionId = await resolveMyraDefinitionIdFromDb( - db, - tenantId, - ); - const chat = await findExistingAgentChat( - { store: chatStore, platform: chatPlatform, tenancy: chatTenancy }, - tenantId, - myraDefinitionId, - ); - return chat?.workbenchId; - } catch (cause) { - // Best-effort fallback lookup (CL-6496): a failure here means - // the task orchestrator falls back further, never that the - // caller sees an error -- but that made this failure invisible - // until reportError existed. Reported, not swallowed. - reportError(cause, { - operation: "resolveFallbackWorkbenchId", - tenantId, - }); - return undefined; - } - }, - }, - }); - // A hand-off claimed by a process that died has no one left to - // redeliver it, so a periodic pass gives up on it and tells the - // person — same shape `credentialExpirySweep` above uses. - const stuckLegSweep = createStuckLegSweep({ - db, - store: taskStore, - notify: taskNotifyDeps, - }); - const chatFinalizedTurnHandler = artifactDeliveryHandlerRef.current; - artifactDeliveryHandlerRef.current = (agentAddress, turn) => { - chatFinalizedTurnHandler?.(agentAddress, turn); - taskOrchestrator.handleFinalizedTurn(agentAddress, turn); + sessionService, + assetService, + sidecarRouter, + eventCollectors, + credentialCipher, + hubPublicKey, + toolGrantsForPins, + mcpCredentialBindingsFor, }; - app.route( - `${TENANT_PREFIX}/tasks`, - createTaskRoutes({ - store: taskStore, - requireGrant: createRequireGrant({ - grantStore: chatGrantStore, - conditionRegistry: chatConditionRegistry, - }), - launch: (input) => launchTask(taskLauncherDeps, input), - }), - ); // Every genuine top-level deployment run, folded runs (workbench hosts, - // invited agents, tasks) excluded — the scoped listing CL-6061 adds + // invited agents) excluded — the scoped listing CL-6061 adds // so the Agent Directory and the shell's "Running" bands stop // deriving that exclusion client-side from a tenant's workbenches alone - // (see `@corbits/folded-runs`'s `scope-routes.ts`, which task-style - // runs — no workbench involved — silently slipped past). The route's + // (see `@corbits/folded-runs`'s `scope-routes.ts`, which a folded run + // with no workbench involved silently slipped past). The route's // `feed=fires` mode (Insights, CL-6249) needs the one bridge // `@corbits/folded-runs` cannot own itself — resolving a folded run id // back to the routine that fired it — wired here, the one place in @@ -2649,11 +2508,7 @@ export async function createHub(config: HubConfig) { // that turns a routine's `launchRoutineRun` call into a real folded // run via `@corbits/folded-runs` (routine-launcher.ts), and a run // summary resolver so `GET /routines/:id/runs` reports each fire's - // real status instead of a bare run id. Constructed after - // `taskLauncherDeps` above (not alongside chat/connections earlier) - // because its `dispatchTask` port needs that object to exist first — - // see routine-launcher.ts's own doc for why a routine ever calls - // `launchTask` at all. + // real status instead of a bare run id. const routineGrantStore = createGrantStore(db); const routineStore = createDrizzleRoutineStore(db); const routineDraftStore = createDrizzleDraftStore(db); @@ -2684,11 +2539,10 @@ export async function createHub(config: HubConfig) { // is never rejected here (`validateTriggerFieldsAtCreate` only // checks a value the caller actually provided), only resolved // further for an `"agent"`-kind field — that a provided value names - // a real taskable definition. An unknown definitionId or asset name - // passes here (its 404 comes from `definitionInTenant` instead); a - // definition with no declared trigger fields accepts any input, same - // as today. `launchTask`'s own definition checks at fire time remain - // the authoritative required-field gate. + // a real conversational definition. An unknown definitionId or asset + // name passes here (its 404 comes from `definitionInTenant` instead); + // a definition with no declared trigger fields accepts any input, same + // as today. async function routineInputValid( tenantId: string, definitionId: string, @@ -2729,7 +2583,7 @@ export async function createHub(config: HubConfig) { ) { return { ok: false, - message: `"${field.label}" must be a taskable agent`, + message: `"${field.label}" must be a conversational agent`, }; } } @@ -2775,12 +2629,11 @@ export async function createHub(config: HubConfig) { return out; } - // A separate `CryptoProviderCache` from the task launcher's and the - // planner's own (`plannerCryptoProviders` below): a routine-drafting - // one-shot run's instance id has nothing to do with either lifecycle, - // so a separate cache keeps the three from ever contending over the - // same key space — same rationale as `plannerCryptoProviders`' own - // comment. + // A separate `CryptoProviderCache` from `foldedRunCryptoProviders` + // above and `agentDefinitionDraftingCryptoProviders` below: a + // routine-drafting one-shot run's instance id has nothing to do with + // either, so a separate cache keeps them from ever contending over the + // same key space. const routineDraftingCryptoProviders = createCryptoProviderCache(); const routineLauncher = createHubRoutineLauncher({ @@ -2793,7 +2646,6 @@ export async function createHub(config: HubConfig) { toolGrantsForPins, mcpCredentialBindingsFor, cryptoProviderCache: foldedRunCryptoProviders, - dispatchTask: (input) => launchTask(taskLauncherDeps, input), joinDeliveryWorkbench: (input) => joinRunParticipant({ store: chatStore }, input), }); @@ -2835,10 +2687,9 @@ export async function createHub(config: HubConfig) { drafts: routineDraftStore, workbenchNotice: routineWorkbenchNotice, // Myra-backed drafting (CL-5917): a real one-shot inference call, - // mirroring `@corbits/task-planner`'s own Myra auto-dispatch - // wiring below (`plannerInventorySources`/`dispatchWithPlanner`) - // — resolve Myra's definition, offer her the automatable-workflow - // and taskable-agent inventory, and never trust her reply beyond + // mirroring the agent-definition drafting wiring below — resolve + // Myra's definition, offer her the automatable-workflow and + // conversational-agent inventory, and never trust her reply beyond // what `@corbits/routines`' own fail-closed validation proves. drafting: createMyraRoutineDrafting({ resolveMyraDefinitionId: (tenantId) => @@ -2847,10 +2698,9 @@ export async function createHub(config: HubConfig) { run: (runnerInput) => runOneShotFoldedPrompt( { - foldedRuns: taskLauncherDeps.foldedRuns, + foldedRuns: oneShotFoldedRunsDeps, events: sidecarRouter.events, cryptoProviders: routineDraftingCryptoProviders, - lifecycle: taskLifecycle, undeploy: (address, reason) => sidecarRouter.sendAgentUndeploy(address, reason), }, @@ -2991,9 +2841,9 @@ export async function createHub(config: HubConfig) { deliveryWorkbenchRequired: routineDeliveryWorkbenchRequired, }); - // Myra auto-dispatch (CL-6051): a typed outcome becomes a validated - // task plan via `@corbits/task-planner`, dispatched exactly like a - // manually-launched task. Every inventory lister below generalizes a + // The inventory Myra is offered when drafting a new agent definition + // (`plannerInventorySources` below, `@corbits/agent-directory`'s own + // `InventorySources` seam). Every inventory lister below generalizes a // pattern that already lives elsewhere in this composition root // (`isConversationalAgentDefinition`, `workbenchHostInferencePreferencesResolver`'s // per-tenant connected-provider derivation) — this package owns the @@ -3119,226 +2969,83 @@ export async function createHub(config: HubConfig) { listModels: listMyraModels, }; - /** - * Wraps the same sequence `@corbits/agent-directory`'s `POST /` - * handler runs (`buildAgentDefinitionWorkflow` → `reindexPinnedSkills` - * when skills are present → `createAsset` + `populateAsset` → - * `freezeInertWorkflowDefinition`), reusing the exact `db`, - * `assetService`, and `skills.skillIndex` already in scope — never a - * second instance of any of them. The one addition beyond that route's - * own input is `toolPackagePins`, which the REST boundary deliberately - * has no field for (see `@corbits/agent-directory`'s `validation.ts`) - * since only this in-process planner caller needs it. - */ - async function deployAgentDefinition(input: { - readonly tenantId: string; - readonly principalId: string; - readonly name: string; - readonly handle: string; - readonly systemPrompt: string; - readonly toolPackagePins: readonly string[]; - readonly skills: readonly string[]; - readonly credentialBindings: readonly CredentialBinding[]; - readonly model?: string; - }): Promise<{ readonly definitionId: string }> { - const tenantRow = await db.query.tenant.findFirst({ - where: eq(tenantTable.id, input.tenantId), - }); - if (tenantRow === undefined) { - throw new Error(`No tenant "${input.tenantId}"`); - } - - const handle = input.handle; - const skillEntries = - input.skills.length > 0 - ? await skills.skillIndex.resolve( - input.tenantId, - input.principalId, - input.skills, - ) - : []; + // An agent-definition drafting one-shot run's instance id has nothing + // to do with a routine draft's, same rationale as + // `routineDraftingCryptoProviders`' own comment above. + const agentDefinitionDraftingCryptoProviders = createCryptoProviderCache(); - type MutableBuildAgentDefinitionInput = { - -readonly [ - K in keyof Parameters[0] - ]: Parameters[0][K]; - }; - const buildInput: MutableBuildAgentDefinitionInput = { - handle, - tenantDomain: tenantRow.domain, - description: "", - systemPrompt: input.systemPrompt, - }; - if (input.model !== undefined) { - buildInput.model = input.model; - } - if (input.toolPackagePins.length > 0) { - buildInput.toolPackagePins = input.toolPackagePins.map((name) => ({ - name, - version: "*", - })); - } - if (input.credentialBindings.length > 0) { - buildInput.credentialBindings = input.credentialBindings; - } - const definition = buildAgentDefinitionWorkflow(buildInput); - const workflowJson = reindexPinnedSkills( - serializeAgentDefinitionWorkflow(definition), - skillEntries, + // The create-agent panel's "Describe" step (CL-6074): a real one-shot + // Myra call that proposes a starting system prompt/tool pins/skills + // from a name + plain-language purpose, offering her the same + // inventory `capabilityInventory` above reads through. Never deploys + // on its own — the panel submits the validated draft through the + // ordinary create-agent-definition path once the person confirms. This + // route is hand-wired here rather than through a factory the package + // exports, since agent-directory's own route factories are each scoped + // to a narrower concern. + const CreateAgentDefinitionDraftBody = type({ + name: "string > 0", + "purpose?": "string > 0", + }); + const agentDefinitionDraftingInFlightPrincipals = new Set(); + function isAgentDefinitionDraftingFailure(err: unknown): boolean { + return ( + err instanceof MyraAgentDefinitionDraftingUnavailableError || + err instanceof FoldedRunTimedOutError || + err instanceof FoldedRunFailedError || + err instanceof AgentDefinitionDraftReplyUnparseableError || + err instanceof AgentDefinitionDraftReferenceOutOfInventoryError ); - - const created = await assetService.createAsset({ - tenantId: input.tenantId, - kind: "workflow", - name: handle, - displayName: input.name, - creatorPrincipalId: input.principalId, - }); - - await assetService.populateAsset({ - assetId: created.id, - ref: DEFAULT_ASSET_REF, - principal: { kind: "hub" }, - tree: { - files: agentDefinitionSourceTree({ handle, workflowJson }), - message: `Define agent ${input.name}`, - }, - }); - await definitionSkillsStore.setSkills(created.id, input.skills); - - // Freeze, not a bare ensure — see `createAgentDefinitionCore`'s own - // freeze call for the why (CL-6447). - const { definitionId } = await freezeInertWorkflowDefinition(db, { - assetId: created.id, - workflowJson, - }); - return { definitionId }; } - - /** - * A chain spawn's cleanup half: flips a definition `deployAgentDefinition` - * just deployed to `workflowDefinition`'s own `"stopped"` status, so a - * step later in the same chain failing to validate or deploy never - * leaves an orphaned agent nothing will ever launch. Scoped to - * `tenantId` + `definitionId` like every other definition write in - * this file; no asset/materialization cleanup, since a `"stopped"` - * definition is already excluded from every launch/taskability path - * `deployAgentDefinition` itself feeds. - */ - async function undeployAgentDefinition(input: { - readonly tenantId: string; - readonly definitionId: string; - }): Promise { - await db - .update(workflowDefinition) - .set({ status: "stopped" }) - .where( - and( - eq(workflowDefinition.tenantId, input.tenantId), - eq(workflowDefinition.id, input.definitionId), - ), + const plannerRoutes = new Hono(); + plannerRoutes.post( + "/agent-definitions/draft", + createRequireGrant({ + grantStore: chatGrantStore, + conditionRegistry: chatConditionRegistry, + })("workflow-definition:*", "create"), + async (c) => { + const body = CreateAgentDefinitionDraftBody( + await c.req.json().catch(() => undefined), ); - } + if (body instanceof type.errors) { + return c.json( + { + error: { + code: "bad_request", + message: `This couldn't be read: ${body.summary}`, + }, + }, + 400, + ); + } - // A separate `CryptoProviderCache` from the task launcher's own - // (`taskLauncherDeps.cryptoProviders`): a planning run's instance id - // is never a real task's, but the cache is keyed by instance id - // regardless, and a planning run's one-shot prompt/reply cadence has - // nothing to do with a launched task's — separate caches keep the - // two lifecycles from ever contending over the same key space. - const plannerCryptoProviders = createCryptoProviderCache(); + const tenant = c.get("tenant"); + const principal = c.get("principal"); - // A separate `CryptoProviderCache` again from `plannerCryptoProviders` - // — an agent-definition drafting one-shot run's instance id has - // nothing to do with either lifecycle, same rationale as - // `routineDraftingCryptoProviders`' own comment above. - const agentDefinitionDraftingCryptoProviders = createCryptoProviderCache(); - - app.route( - `${TENANT_PREFIX}/planner`, - createPlannerRoutes({ - requireGrant: createRequireGrant({ - grantStore: chatGrantStore, - conditionRegistry: chatConditionRegistry, - }), - dispatch: (input) => - dispatchWithPlanner( + if (agentDefinitionDraftingInFlightPrincipals.has(principal.id)) { + return c.json( { - db, - runner: { - run: (runnerInput) => - runOneShotFoldedPrompt( - { - foldedRuns: taskLauncherDeps.foldedRuns, - events: sidecarRouter.events, - cryptoProviders: plannerCryptoProviders, - // Reuses `taskLifecycle` rather than standing up a - // second idle-sleep instance: it's keyed entirely by - // address, and a planner run's `triggerAddress` - // (`formatRunAddress` over a freshly generated - // `workflowRun` instance id) can never collide with a - // task's — sharing costs nothing and keeps one sweep - // instead of two. - lifecycle: taskLifecycle, - undeploy: (address, reason) => - sidecarRouter.sendAgentUndeploy(address, reason), - }, - runnerInput, - ), - }, - inventorySources: plannerInventorySources, - resolveMyraDefinitionId: (tenantId) => - resolveMyraDefinitionIdFromDb(db, tenantId), - taskLauncherDeps, - store: taskStore, - deployAgentDefinition, - undeployAgentDefinition, - // The `{create}` branch's own grant, checked deep inside - // `dispatch` rather than at route-middleware time — the - // definitional plan (`{use}` vs `{create}`) is only known - // after Myra's reply resolves. Same `chatGrantStore`/ - // `chatConditionRegistry` every other `requireGrant` call - // site in this file uses, called through `authorize` - // directly (the standalone, non-middleware primitive - // `createRequireGrant`'s own middleware wraps) since this - // is not a route boundary. - requireDefinitionCreateGrant: async ({ tenantId, principalId }) => { - const result = await authorize( - chatGrantStore, - principalId, - tenantId, - "workflow-definition:*", - "create", - chatConditionRegistry, - ); - if (result.effect !== "allow") { - throw new PlannerDefinitionGrantDeniedError(principalId); - } + error: { + code: "dispatch_in_progress", + message: "Myra is already drafting your last agent.", }, }, - input, - ), - // Agent-definition drafting (CL-6074): the create-agent panel's - // "Create & chat" flow asks Myra for a starting system prompt from - // a name + plain-language purpose, mirroring the routine-drafting - // wiring above — resolve Myra's definition, offer her the same - // inventory the planner itself uses, and never trust her reply - // beyond what `@corbits/task-planner`'s own fail-closed validation - // proves. Never deploys on its own; the panel submits the - // validated draft through the ordinary create-agent-definition - // path once the person confirms. - draftAgentDefinition: (input) => - createMyraAgentDefinitionDrafting({ + 409, + ); + } + agentDefinitionDraftingInFlightPrincipals.add(principal.id); + try { + const draft = await createMyraAgentDefinitionDrafting({ resolveMyraDefinitionId: (tenantId) => resolveMyraDefinitionIdFromDb(db, tenantId), runner: { run: (runnerInput) => runOneShotFoldedPrompt( { - foldedRuns: taskLauncherDeps.foldedRuns, + foldedRuns: oneShotFoldedRunsDeps, events: sidecarRouter.events, cryptoProviders: agentDefinitionDraftingCryptoProviders, - lifecycle: taskLifecycle, undeploy: (address, reason) => sidecarRouter.sendAgentUndeploy(address, reason), }, @@ -3346,58 +3053,33 @@ export async function createHub(config: HubConfig) { ), }, inventorySources: plannerInventorySources, - }).propose(input), - }), - ); - // Myra's own task-dispatch surface (`@corbits/task-dispatch-tools`' - // `dispatch_task`): the workflow-run-authenticated counterpart to the - // tenant-session planner route just above, reusing the exact same - // spawn/planner deps. When the tool call names an `agentDefinitionId` - // it skips the planner's own one-shot re-ask entirely (see - // `@corbits/task-planner`'s `workflow-dispatch-routes.ts`); otherwise - // it falls back to the full Myra-picks-or-creates-an-agent flow. - app.route( - "/api/workflow-task-planner", - createWorkflowDispatchRoutes({ - authenticator: createWorkflowRunAuthenticator({ db }), - db, - runner: { - run: (runnerInput) => - runOneShotFoldedPrompt( + }).propose({ + tenantId: tenant.id, + principalId: principal.id, + name: body.name, + ...(body.purpose !== undefined ? { purpose: body.purpose } : {}), + }); + return c.json({ draft }, 201); + } catch (err) { + if (isAgentDefinitionDraftingFailure(err)) { + return c.json( { - foldedRuns: taskLauncherDeps.foldedRuns, - events: sidecarRouter.events, - cryptoProviders: plannerCryptoProviders, - lifecycle: taskLifecycle, - undeploy: (address, reason) => - sidecarRouter.sendAgentUndeploy(address, reason), + error: { + code: "drafting_failed", + message: + "Myra couldn't draft a starting prompt for that. Write one yourself, or try again.", + }, }, - runnerInput, - ), - }, - inventorySources: plannerInventorySources, - resolveMyraDefinitionId: (tenantId) => - resolveMyraDefinitionIdFromDb(db, tenantId), - taskLauncherDeps, - store: taskStore, - chatStore, - deployAgentDefinition, - undeployAgentDefinition, - requireDefinitionCreateGrant: async ({ tenantId, principalId }) => { - const result = await authorize( - chatGrantStore, - principalId, - tenantId, - "workflow-definition:*", - "create", - chatConditionRegistry, - ); - if (result.effect !== "allow") { - throw new PlannerDefinitionGrantDeniedError(principalId); + 422, + ); } - }, - }), + throw err; + } finally { + agentDefinitionDraftingInFlightPrincipals.delete(principal.id); + } + }, ); + app.route(`${TENANT_PREFIX}/planner`, plannerRoutes); // The sanctioned path for a workflow run to reach the memory plane // (CL-5852), mirroring `/api/workflow-artifacts` immediately above: @@ -3703,9 +3385,6 @@ export async function createHub(config: HubConfig) { } envCredentialPlant.stop(); chatOrchestrator.dispose(); - taskOrchestrator.dispose(); - taskLifecycle.stop(); - stuckLegSweep.stop(); routineScheduler.stop(); credentialExpirySweep.stop(); benchProvisioner.stop(); diff --git a/apps/hub/src/routine-launcher.test.ts b/apps/hub/src/routine-launcher.test.ts index d3a0c2713..9998ee7cc 100644 --- a/apps/hub/src/routine-launcher.test.ts +++ b/apps/hub/src/routine-launcher.test.ts @@ -11,7 +11,6 @@ // `packages/webhook-triggers/test/launch.test.ts` use. import { describe, expect, mock, test } from "bun:test"; import { CHAT_TURN_TIMEOUT_MS } from "@corbits/chat"; -import { RECURRING_TASK_ASSET_NAME } from "@corbits/workflow-catalog"; const actualFoldedRuns = await import("@corbits/folded-runs"); @@ -131,18 +130,6 @@ function baseInput(input: Record) { }; } -let dispatchTaskCalls: unknown[] = []; -let dispatchTaskResult: unknown = { runId: "wfr_task1" }; -let dispatchTaskShouldThrow: Error | null = null; - -function dispatchTask(input: unknown) { - dispatchTaskCalls.push(input); - if (dispatchTaskShouldThrow !== null) { - return Promise.reject(dispatchTaskShouldThrow); - } - return Promise.resolve(dispatchTaskResult as never); -} - let joinDeliveryWorkbenchCalls: unknown[] = []; function buildLauncher(overrides: { definition?: unknown } = {}) { @@ -162,7 +149,6 @@ function buildLauncher(overrides: { definition?: unknown } = {}) { toolGrantsForPins: () => [], eventCollectors: {} as never, cryptoProviderCache: { get: async () => ({}) as never }, - dispatchTask: dispatchTask as never, }); } @@ -269,14 +255,6 @@ describe("createHubRoutineLauncher", () => { }); }); -const RECURRING_TASK_DEFINITION_ROW = { - id: "wfd_recurring", - tenantId: "ten_1", - status: "deployed" as const, - assetId: "ast_recurring", - name: RECURRING_TASK_ASSET_NAME, -}; - describe("createHubRoutineLauncher — delivery workbench", () => { test("joins the launched run into the routine's delivery workbench so the orchestrator posts its replies there", async () => { launchFoldedRunCalls = []; @@ -317,7 +295,6 @@ describe("createHubRoutineLauncher — delivery workbench", () => { toolGrantsForPins: () => [], eventCollectors: {} as never, cryptoProviderCache: { get: async () => ({}) as never }, - dispatchTask: dispatchTask as never, }); const result = await launcher.launchRoutineRun({ ...baseInput({}), @@ -327,147 +304,6 @@ describe("createHubRoutineLauncher — delivery workbench", () => { }); }); -describe("createHubRoutineLauncher — recurring-task bridge", () => { - test("dispatches through dispatchTask instead of launching its own folded run", async () => { - launchFoldedRunCalls = []; - dispatchTaskCalls = []; - dispatchTaskShouldThrow = null; - dispatchTaskResult = { runId: "wfr_task1" }; - - const result = await createHubRoutineLauncher({ - joinDeliveryWorkbench: async () => {}, - db: createFakeDb({ definition: RECURRING_TASK_DEFINITION_ROW }) as never, - sessionService: {} as never, - assetService: {} as never, - sidecarRouter: {} as never, - toolGrantsForPins: () => [], - eventCollectors: {} as never, - cryptoProviderCache: { get: async () => ({}) as never }, - dispatchTask: dispatchTask as never, - }).launchRoutineRun( - baseInput({ - agent: "wfd_summarizer", - prompt: "Summarize last night's incidents", - }), - ); - - expect(result.runId).toBe("wfr_task1"); - // The placeholder definition's own folded run is never launched — - // the whole point of the bridge is to skip it. - expect(launchFoldedRunCalls).toHaveLength(0); - expect(dispatchTaskCalls).toEqual([ - { - tenantId: "ten_1", - principalId: "usr_1", - definitionId: "wfd_summarizer", - prompt: "Summarize last night's incidents", - }, - ]); - }); - - test("a deleted/unknown agent fails the routine run honestly, propagating dispatchTask's own error", async () => { - dispatchTaskCalls = []; - dispatchTaskShouldThrow = new Error( - 'No definition "wfd_deleted" for this tenant', - ); - - const launcher = createHubRoutineLauncher({ - joinDeliveryWorkbench: async () => {}, - db: createFakeDb({ definition: RECURRING_TASK_DEFINITION_ROW }) as never, - sessionService: {} as never, - assetService: {} as never, - sidecarRouter: {} as never, - toolGrantsForPins: () => [], - eventCollectors: {} as never, - cryptoProviderCache: { get: async () => ({}) as never }, - dispatchTask: dispatchTask as never, - }); - - await expect( - launcher.launchRoutineRun( - baseInput({ agent: "wfd_deleted", prompt: "Do the thing" }), - ), - ).rejects.toThrow('No definition "wfd_deleted" for this tenant'); - - dispatchTaskShouldThrow = null; - }); - - test("behaves the same whether the agent id is a manually-created or planner-created (myra-task-*) definition — no shape validation beyond non-empty", async () => { - dispatchTaskCalls = []; - dispatchTaskShouldThrow = null; - dispatchTaskResult = { runId: "wfr_task2" }; - - const result = await createHubRoutineLauncher({ - joinDeliveryWorkbench: async () => {}, - db: createFakeDb({ definition: RECURRING_TASK_DEFINITION_ROW }) as never, - sessionService: {} as never, - assetService: {} as never, - sidecarRouter: {} as never, - toolGrantsForPins: () => [], - eventCollectors: {} as never, - cryptoProviderCache: { get: async () => ({}) as never }, - dispatchTask: dispatchTask as never, - }).launchRoutineRun( - baseInput({ - agent: "myra-task-a1b2c3", - prompt: "Draft the weekly digest", - }), - ); - - expect(result.runId).toBe("wfr_task2"); - expect(dispatchTaskCalls).toEqual([ - { - tenantId: "ten_1", - principalId: "usr_1", - definitionId: "myra-task-a1b2c3", - prompt: "Draft the weekly digest", - }, - ]); - }); - - test("refuses to dispatch when the stored input is missing its agent field", async () => { - dispatchTaskCalls = []; - - const launcher = createHubRoutineLauncher({ - joinDeliveryWorkbench: async () => {}, - db: createFakeDb({ definition: RECURRING_TASK_DEFINITION_ROW }) as never, - sessionService: {} as never, - assetService: {} as never, - sidecarRouter: {} as never, - toolGrantsForPins: () => [], - eventCollectors: {} as never, - cryptoProviderCache: { get: async () => ({}) as never }, - dispatchTask: dispatchTask as never, - }); - - await expect( - launcher.launchRoutineRun(baseInput({ prompt: "Do the thing" })), - ).rejects.toThrow(/agent/); - expect(dispatchTaskCalls).toHaveLength(0); - }); - - test("refuses to dispatch when the stored input is missing its prompt field", async () => { - dispatchTaskCalls = []; - - const launcher = createHubRoutineLauncher({ - joinDeliveryWorkbench: async () => {}, - db: createFakeDb({ definition: RECURRING_TASK_DEFINITION_ROW }) as never, - sessionService: {} as never, - assetService: {} as never, - sidecarRouter: {} as never, - toolGrantsForPins: () => [], - eventCollectors: {} as never, - cryptoProviderCache: { get: async () => ({}) as never }, - dispatchTask: dispatchTask as never, - }); - - await expect( - launcher.launchRoutineRun(baseInput({ agent: "wfd_summarizer" })), - ).rejects.toThrow(/prompt/); - expect(dispatchTaskCalls).toHaveLength(0); - }); -}); - describe("createHubRoutineLauncher — multi-step native routing", () => { test("routes a multi-step definition onto the native trigger instead of the folded launcher", async () => { foldedBodyMode = "multi"; @@ -559,7 +395,6 @@ describe("createHubRoutineLauncher — multi-step native routing", () => { toolGrantsForPins: () => [], eventCollectors: {} as never, cryptoProviderCache: { get: async () => ({}) as never }, - dispatchTask: dispatchTask as never, }); await expect(launcher.launchRoutineRun(baseInput({}))).rejects.toThrow( diff --git a/apps/hub/src/routine-launcher.ts b/apps/hub/src/routine-launcher.ts index e7818e97b..4f95e2e9f 100644 --- a/apps/hub/src/routine-launcher.ts +++ b/apps/hub/src/routine-launcher.ts @@ -26,22 +26,6 @@ // this function still returns the run id on a delivery failure, after // exhausting `sendFoldedMailWithRetry`'s bounded retries, and only logs // (naming the run) rather than throwing. -// -// One definition short-circuits this whole folded-run path: -// `RECURRING_TASK_ASSET_NAME` (`@corbits/workflow-catalog`) is the -// "Make this a routine" bridge (an Inbox action on a completed task -// result — apps/web/src/pages/inbox-page.tsx) — a task's own agent is -// conversational, never automatable, so it can never be a routine's -// `definitionId` itself. This placeholder definition exists only to give -// the Routines picker a real, automatable id to schedule; firing it -// never launches its own folded run at all. Instead, the routine's -// stored `agent`/`prompt` trigger-field input goes straight through -// `dispatchTask` — the exact same `@corbits/tasks` `launchTask` call -// `POST /tasks` uses — so a scheduled recurring task lands in the -// creator's Inbox exactly like a manual one, on the same launch path, no -// duplicated logic. A `deliveryWorkbenchId` picked on the routine (if any) -// is not used for this delivery: a task result never posts to a workbench, -// only to the Inbox. import { and, eq } from "drizzle-orm"; import type { DB } from "@intx/db"; import { tenant as tenantTable, workflowDefinition } from "@intx/db/schema"; @@ -65,8 +49,6 @@ import { workbenchLaunchPersistExtra, } from "@corbits/chat"; import { renderRoutineInput, type RoutineLauncher } from "@corbits/routines"; -import { RECURRING_TASK_ASSET_NAME } from "@corbits/workflow-catalog"; -import type { LaunchTaskInput, TaskRecord } from "@corbits/tasks"; import { triggerNativeWorkflowRoutineRun } from "./native-workflow-routine-launch"; const log = getLogger(["hub", "routine-launcher"]); @@ -75,12 +57,6 @@ export type CreateHubRoutineLauncherDeps = FoldedRunsDeps & { db: DB["db"]; assetService: AssetService; cryptoProviderCache: CryptoProviderCache; - /** The narrow port a fired recurring-task routine dispatches through - * — hub wires this to `(input) => launchTask(taskLauncherDeps, input)`, - * the same deps object `POST /tasks` calls with. Domain logic (what a - * task launch is) stays entirely in `@corbits/tasks`; this port is - * pure composition, same as every other dep here. */ - dispatchTask: (input: LaunchTaskInput) => Promise; /** Adds the launched run's address to the routine's delivery workbench * as a participant — hub wires this to `@corbits/chat`'s * `joinRunParticipant`. Membership is what makes the chat @@ -96,31 +72,10 @@ export type CreateHubRoutineLauncherDeps = FoldedRunsDeps & { }) => Promise; }; -function recurringTaskFieldsFromInput(input: Record): { - agent: string; - prompt: string; -} { - const agent = input["agent"]; - const prompt = input["prompt"]; - if (typeof agent !== "string" || agent === "") { - throw new Error( - 'recurring-task routine is missing its "agent" trigger-field input', - ); - } - if (typeof prompt !== "string" || prompt === "") { - throw new Error( - 'recurring-task routine is missing its "prompt" trigger-field input', - ); - } - return { agent, prompt }; -} - /** * Builds the hub's `RoutineLauncher`: every routine fire — "run now" or * scheduled — resolves to exactly this launch path, the same folded-run - * launch every other agent instance in this hub goes through (except a - * recurring-task routine, which dispatches through `dispatchTask` - * instead — see this module's own doc comment). + * launch every other agent instance in this hub goes through. */ export function createHubRoutineLauncher( deps: CreateHubRoutineLauncherDeps, @@ -150,17 +105,6 @@ export function createHubRoutineLauncher( ); } - if (definitionRow.name === RECURRING_TASK_ASSET_NAME) { - const { agent, prompt } = recurringTaskFieldsFromInput(input.input); - const task = await deps.dispatchTask({ - tenantId: input.tenantId, - principalId: input.principalId, - definitionId: agent, - prompt, - }); - return { runId: task.runId }; - } - const tenantRow = await deps.db.query.tenant.findFirst({ where: eq(tenantTable.id, input.tenantId), }); diff --git a/apps/hub/test/routine-scheduler.test.ts b/apps/hub/test/routine-scheduler.test.ts index 2a3bb76ea..f6c3d523f 100644 --- a/apps/hub/test/routine-scheduler.test.ts +++ b/apps/hub/test/routine-scheduler.test.ts @@ -52,8 +52,8 @@ describe("tickRoutineScheduler", () => { const store = createInMemoryRoutineStore(); const routine = await store.createRoutine({ tenantId: "t1", - name: "recurring task", - definitionId: "def_recurring_task", + name: "inbox-only task", + definitionId: "def_inbox_only", trigger: CRON, scope: "bench", input: { agent: "wfd_agent", prompt: "Do it" }, @@ -74,7 +74,7 @@ describe("tickRoutineScheduler", () => { }, at, ); - expect(launches).toEqual(["def_recurring_task"]); + expect(launches).toEqual(["def_inbox_only"]); const runs = await store.listRunsForRoutine("t1", routine.id); expect(runs).toHaveLength(1); }); diff --git a/apps/sidecar/src/workflow-substrate-factory/step-env.ts b/apps/sidecar/src/workflow-substrate-factory/step-env.ts index 981fcf6f9..7dbe1fdcc 100644 --- a/apps/sidecar/src/workflow-substrate-factory/step-env.ts +++ b/apps/sidecar/src/workflow-substrate-factory/step-env.ts @@ -350,7 +350,6 @@ export function createSidecarStepBuildEnv( hubSkillsUrl: string; hubCapabilitiesUrl: string; hubRoutinesUrl: string; - hubTaskPlannerUrl: string; hubConnectionsUrl: string; hubCatalogUrl: string; hubAgentDirectoryUrl: string; @@ -396,13 +395,12 @@ export function createSidecarStepBuildEnv( // surface. hubCapabilitiesUrl: deps.hubArtifactsUrl, // Same hub HTTP origin again, under the keys the Myra manager-tools - // bundles declare (CL-5879 follow-up: routines-tools, task-dispatch- - // tools, connections-tools, agent-directory-tools) for their own + // bundles declare (CL-5879 follow-up: routines-tools, + // connections-tools, agent-directory-tools) for their own // workflow-run-authenticated surfaces — one hub origin, one env key // per tool-bundle surface, matching every precedent above rather // than overloading an existing name for an unrelated surface. hubRoutinesUrl: deps.hubArtifactsUrl, - hubTaskPlannerUrl: deps.hubArtifactsUrl, hubConnectionsUrl: deps.hubArtifactsUrl, // And once more under the key `@corbits/catalog-tools` declares // (`requires: ["hubCatalogUrl", "sidecarToken", "address"]`) for the diff --git a/apps/web/package.json b/apps/web/package.json index 6095c293b..987c1a2c4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -37,7 +37,6 @@ "@corbits/settings-ui": "workspace:*", "@corbits/shell-layout": "workspace:*", "@corbits/slug": "workspace:*", - "@corbits/tasks-ui": "workspace:*", "@corbits/text-diff": "workspace:*", "@corbits/url-path": "workspace:*", "@corbits/workflow-catalog": "workspace:*", diff --git a/apps/web/src/agents-api.ts b/apps/web/src/agents-api.ts index 3651d7b04..0343027a5 100644 --- a/apps/web/src/agents-api.ts +++ b/apps/web/src/agents-api.ts @@ -135,7 +135,7 @@ export function listAgentInstances( /** * The tenant's genuine top-level deployment runs — every folded run - * (workbench host, invited agent, task) excluded server-side by the hub's + * (workbench host, invited agent) excluded server-side by the hub's * own `folded_run` marker table (see `@corbits/folded-runs`'s * `scope-routes.ts`), not derived client-side from a tenant's workbenches * the way `foldedRunIdsFromWorkbenches` used to. Used wherever a page needs @@ -181,7 +181,7 @@ export type AgentDefinitionDraft = * Asks Myra to draft a starting system prompt (and optionally a * refined description, a model pick, and skills) from a name and a * plain-language purpose — the create-agent panel's "Create & chat" - * flow (CL-6074). Hits `@corbits/task-planner`'s + * flow (CL-6074). Hits `@corbits/agent-directory`'s * `POST .../planner/agent-definitions/draft`; never deploys anything * itself. A caller that gets a rejected promise here (Myra unavailable, * the draft timing out, an unparseable or out-of-inventory reply, or a @@ -408,7 +408,7 @@ type ModelsOutcome = * the model catalog and each definition's attached skills are best-effort * so either failing alone never blanks the page. `instances` comes from * `listTopLevelRuns`, which already excludes every folded run (workbench - * host, invited agent, task) server-side — see `@corbits/folded-runs`'s + * host, invited agent) server-side — see `@corbits/folded-runs`'s * `scope-routes.ts` — so this page never has to derive that exclusion * itself from a tenant's workbenches. */ diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 0d25058ba..e8eec4703 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -9,7 +9,6 @@ @import "@corbits/chat-ui/styles.css"; @import "@corbits/bench-ui/styles.css"; @import "@corbits/settings-ui/styles.css"; -@import "@corbits/tasks-ui/styles.css"; @import "@corbits/plugins-ui/styles.css"; /* react-ui's `.corbits-toast` ships a hardcoded horizontal-center transform @@ -416,8 +415,7 @@ select:disabled, } /* Mock .section labels in sidebar lists — always 0.65rem / 0.07em. */ -.shell-sidebar .panel-band-heading, -.shell-sidebar .panel-band-subheading { +.shell-sidebar .panel-band-heading { font-size: 0.65rem; font-weight: 650; letter-spacing: 0.07em; @@ -546,16 +544,6 @@ select:disabled, color: var(--muted-foreground); } -.panel-band-subheading { - margin: 0; - padding: 0.25rem 0.5rem 0.125rem; - font-size: 0.65rem; - font-weight: 650; - letter-spacing: 0.07em; - text-transform: uppercase; - color: var(--muted-foreground); -} - .panel-muted { margin: 0; font-size: 0.75rem; @@ -2926,17 +2914,6 @@ tr.insights-row-clickable:hover { color: var(--shell-panel-faint); } -/* A working-task row's meta slot (`@corbits/tasks-ui`'s `WorkingTaskRow`): - elapsed time beside the optional needs-you badge. */ -.panel-task-meta { - display: inline-flex; - align-items: center; - gap: 0.35rem; - font-size: 0.6875rem; - font-weight: 500; - color: var(--shell-panel-faint); -} - /* Global context menu (`shell/context-menu`): the keyboard shortcut hint right-aligned in a menu row. Everything else about the menu's look is react-ui's Menu component. */ diff --git a/apps/web/src/insights-api.ts b/apps/web/src/insights-api.ts index 0e1c961a1..35a954009 100644 --- a/apps/web/src/insights-api.ts +++ b/apps/web/src/insights-api.ts @@ -12,11 +12,7 @@ // workbenches. usage/activity/tools roll up automatically when called // with a parent's tenantId (see @corbits/insights' resolveScope) — // /scope is how a page discovers that shape to build a switcher. -// Plus two tenant-scoped routes outside `/insights`, reused for chain -// context on a run's detail view (packages/tasks/src/routes.ts): -// GET /tasks/by-run/:runId → { item: Task } | 404 (run has no owning task) -// GET /tasks/:id/legs → { items: TaskLeg[] } in position order -// and one more reused as Insights' run feed: GET /top-level-runs?feed=fires +// Plus one more reused as Insights' run feed: GET /top-level-runs?feed=fires // → Paginated // (packages/folded-runs/src/scope-routes.ts's `listTopLevelRunFires`). @@ -228,53 +224,6 @@ export function insightsScopePath(tenantId: string): string { return `/api/tenants/${tenantId}/insights/scope`; } -/** GET /tasks/:id/legs item — one step of a chained task. */ -export const TaskLegSchema = type({ - position: "number", - definitionId: "string", - prompt: "string", - status: "'pending' | 'dispatching' | 'running' | 'done' | 'failed'", - runId: "string | null", - startedAt: "string | null", - settledAt: "string | null", -}); - -/** GET /tasks/:id/legs envelope. */ -export const TaskLegsResponseSchema = type({ - items: TaskLegSchema.array(), -}); - -/** GET /tasks/:id and GET /tasks/by-run/:runId item — the chain a run - * belongs to, when it belongs to one. */ -export const TaskSchema = type({ - id: "string", - definitionId: "string", - agentName: "string", - prompt: "string", - status: "string", - runId: "string", - runIds: "string[]", - stepCount: "number", -}); - -/** GET /tasks/by-run/:runId envelope — `item` is null for a run with no - * owning task (every directly-launched run), a normal state the server - * answers 200 for, never 404. */ -export const TaskResponseSchema = type({ - item: TaskSchema.or("null"), -}); - -export type TaskLeg = typeof TaskLegSchema.infer; -export type Task = typeof TaskSchema.infer; - -export function insightsTaskByRunPath(tenantId: string, runId: string): string { - return `/api/tenants/${tenantId}/tasks/by-run/${encodeURIComponent(runId)}`; -} - -export function insightsTaskLegsPath(tenantId: string, taskId: string): string { - return `/api/tenants/${tenantId}/tasks/${encodeURIComponent(taskId)}/legs`; -} - /** * `WorkflowRunResponse` plus the two fields only the `feed=fires` mode of * `/top-level-runs` reports (CL-6249): the routine that fired this run, diff --git a/apps/web/src/insights-stats.test.ts b/apps/web/src/insights-stats.test.ts index 76a07194b..36f4beb53 100644 --- a/apps/web/src/insights-stats.test.ts +++ b/apps/web/src/insights-stats.test.ts @@ -5,14 +5,11 @@ import { computeTraceStats, filterRunsByCreatedAt, groupRunsByDefinition, - legDurationMs, - legStatusTone, purposeRunsForInsights, runDisplayName, } from "./insights-stats"; import type { InsightsRun, RunTraceSpan } from "./insights-api"; import type { Routine } from "./routines-api"; -import type { TaskLeg } from "./insights-api"; function span( partial: Partial & Pick, @@ -165,18 +162,6 @@ describe("computeTraceStats", () => { }); }); -function leg(partial: Partial & Pick): TaskLeg { - return { - definitionId: "wfd_agent", - prompt: "do the thing", - status: "pending", - runId: null, - startedAt: null, - settledAt: null, - ...partial, - }; -} - describe("groupRunsByDefinition", () => { test("groups runs by definitionId, newest first within each group", () => { const groups = groupRunsByDefinition([ @@ -299,46 +284,6 @@ describe("runDisplayName", () => { }); }); -describe("legDurationMs", () => { - test("derives duration from startedAt/settledAt", () => { - expect( - legDurationMs( - leg({ - position: 0, - startedAt: "2026-01-01T00:00:00.000Z", - settledAt: "2026-01-01T00:00:05.000Z", - }), - ), - ).toBe(5000); - }); - - test("returns null when the leg has not settled", () => { - expect( - legDurationMs( - leg({ - position: 0, - startedAt: "2026-01-01T00:00:00.000Z", - settledAt: null, - }), - ), - ).toBeNull(); - }); - - test("returns null when the leg never started", () => { - expect(legDurationMs(leg({ position: 0 }))).toBeNull(); - }); -}); - -describe("legStatusTone", () => { - test("maps each leg status to a badge tone", () => { - expect(legStatusTone("pending")).toBe("neutral"); - expect(legStatusTone("dispatching")).toBe("info"); - expect(legStatusTone("running")).toBe("info"); - expect(legStatusTone("done")).toBe("success"); - expect(legStatusTone("failed")).toBe("danger"); - }); -}); - describe("filterRunsByCreatedAt", () => { const from = "2026-01-08T18:00:00.000Z"; const to = "2026-01-15T18:00:00.000Z"; diff --git a/apps/web/src/insights-stats.ts b/apps/web/src/insights-stats.ts index b8605a58b..71ce56340 100644 --- a/apps/web/src/insights-stats.ts +++ b/apps/web/src/insights-stats.ts @@ -3,9 +3,8 @@ // existing endpoints. import { isWorkbenchHostDefinitionName } from "@corbits/chat/workbench-host-naming"; -import type { BadgeTone } from "@corbits/react-ui"; -import type { InsightsRun, RunTraceSpan, TaskLeg } from "./insights-api"; +import type { InsightsRun, RunTraceSpan } from "./insights-api"; import type { Routine } from "./routines-api"; export type TraceStats = { @@ -140,32 +139,6 @@ export function groupRunsByDefinition( ); } -/** Duration of a settled leg in milliseconds, or null while it is still - * pending, dispatching, or running — never a fabricated in-flight number. */ -export function legDurationMs(leg: TaskLeg): number | null { - if (leg.startedAt === null || leg.settledAt === null) return null; - const startMs = Date.parse(leg.startedAt); - const endMs = Date.parse(leg.settledAt); - if (Number.isNaN(startMs) || Number.isNaN(endMs)) return null; - return Math.max(0, endMs - startMs); -} - -/** Badge tone for a chain step, matching the run-status palette used - * elsewhere on Insights (`statusTone` in `pages/insights-page.tsx`). */ -export function legStatusTone(status: TaskLeg["status"]): BadgeTone { - switch (status) { - case "done": - return "success"; - case "dispatching": - case "running": - return "info"; - case "failed": - return "danger"; - case "pending": - return "neutral"; - } -} - export function filterRunsByCreatedAt( runs: readonly InsightsRun[], fromIso: string, diff --git a/apps/web/src/pages/create-agent-panel.tsx b/apps/web/src/pages/create-agent-panel.tsx index 9133066da..281edf904 100644 --- a/apps/web/src/pages/create-agent-panel.tsx +++ b/apps/web/src/pages/create-agent-panel.tsx @@ -16,7 +16,7 @@ // is optional, a quiet secondary field, not a gate. Either way, one // click on "Get started" asks Myra to draft a starting system prompt // (and optionally a description/model/skills) via `draftAgentDefinition` -// (`@corbits/task-planner`'s one-shot drafting port, CL-6074) — with a +// (`@corbits/agent-directory`'s one-shot drafting port, CL-6074) — with a // purpose, from that brief; with none, a friendly general-purpose draft // the person teaches in the conversation that follows — then deploys // with that draft. The Suggestions row below the button is the same diff --git a/apps/web/src/pages/insights-page.tsx b/apps/web/src/pages/insights-page.tsx index 93a9485ae..62c08fe41 100644 --- a/apps/web/src/pages/insights-page.tsx +++ b/apps/web/src/pages/insights-page.tsx @@ -75,8 +75,6 @@ import { LatencySummarySchema, OverallUsageSchema, RunTraceSchema, - TaskLegsResponseSchema, - TaskResponseSchema, ToolsResponseSchema, TopLevelRunsSchema, WorkbenchesResponseSchema, @@ -84,8 +82,6 @@ import { insightsLatencyPath, insightsRunTracePath, insightsScopePath, - insightsTaskByRunPath, - insightsTaskLegsPath, insightsToolsPath, insightsTopLevelRunsPath, insightsUsagePath, @@ -94,7 +90,6 @@ import { type InsightsScope, type LatencySummary, type RunTrace, - type TaskLeg, type ToolCall, type WorkbenchUsage, } from "../insights-api"; @@ -103,8 +98,6 @@ import { computeTraceStats, filterRunsByCreatedAt, groupRunsByDefinition, - legDurationMs, - legStatusTone, purposeRunsForInsights, runDisplayName, } from "../insights-stats"; @@ -1058,97 +1051,12 @@ export function InsightsRunsHistory({ ); } -// A chain step strip for a linear task chain only — CL-5514 tracks parent → -// child dispatch trees (real branching, not a straight hand-off sequence) -// as future scope; this renders `legs` as a flat ordered sequence and -// assumes there is exactly one "next" leg per position, same as the route -// this reads from (`GET /tasks/:id/legs` in packages/tasks/src/routes.ts). -// The strip reflects the legs query's own cache — up to 30s stale, or -// refreshed on window refocus (see `createAppQueryClient` in -// `../query-client.ts`) — not a live subscription; for a chain that is -// actively progressing that's a deliberate read-mostly trade-off, not a bug. -function TaskChainStrip({ - legs, - currentRunId, - onOpenRun, -}: { - readonly legs: readonly TaskLeg[]; - readonly currentRunId: string; - readonly onOpenRun: (runId: string) => void; -}) { - return ( -
-

Chain steps

-
    - {legs.map((leg) => { - const isCurrent = leg.runId === currentRunId; - const durationMs = legDurationMs(leg); - const body = ( - <> - - {`Step ${leg.position + 1} of ${legs.length}`} - - {leg.status} - - {durationMs === null ? "—" : durationLabel(durationMs)} - - - ); - if (leg.runId === null) { - return ( -
  1. - {body} -
  2. - ); - } - return ( -
  3. - -
  4. - ); - })} -
-
- ); -} - export function InsightsRunDetail({ - runId, run, trace, - chainLegs, - chainLookupFailed = false, - onOpenRun, }: { - readonly runId: string; readonly run: InsightsRun | null; readonly trace: APIQuery; - /** Ordered legs for the owning task, or null when this run has no - * owning task, or the task has never been fetched. A single-leg task's - * legs are still passed through — the strip itself hides for - * `legs.length <= 1` so a chain-less run renders unchanged. */ - readonly chainLegs: readonly TaskLeg[] | null; - /** True when the by-run chain-context lookup failed — a 500 or a - * network failure ("no owning task" is a normal `{item: null}` 200, - * not a failure). Never silently omitted: renders a small honest note - * instead of just leaving the strip out. */ - readonly chainLookupFailed?: boolean; - readonly onOpenRun: (runId: string) => void; }) { const spans = trace.kind === "ready" ? toTraceSpans(trace.data) : []; const traceStats = @@ -1168,18 +1076,6 @@ export function InsightsRunDetail({
- {chainLegs !== null && chainLegs.length > 1 ? ( - - ) : null} - {chainLookupFailed ? ( -

- Couldn't check this run's task context. -

- ) : null} {/* Owner is not carried by WorkflowRunResponse yet — dash, not a fabricated identity. */} @@ -1461,9 +1357,6 @@ export function InsightsPage({ runId={runId} run={run} tenantId={selectedTenantId} - onOpenRun={(id) => - navigate(`${INSIGHTS_RUNS_PATH}/${encodeURIComponent(id)}`) - } /> ); } @@ -1551,47 +1444,17 @@ export function InsightsRunDetailRoute({ runId, run, tenantId, - onOpenRun, }: { readonly runId: string; readonly run: InsightsRun | null; readonly tenantId: string | null; - readonly onOpenRun: (runId: string) => void; }) { const trace = useAPIQuery( tenantId === null ? "" : insightsRunTracePath(tenantId, runId), RunTraceSchema, ); - // Chain context (CL-5626): resolve the owning task from this run, then - // its legs, so a run reached from a task/inbox link shows its chain. A - // run with no owning task answers `{item: null}` — a normal 200, so the - // plain single-run view renders quietly. Any failure (500, network) is - // a real problem and must say so — see `chainLookupFailed` below. - const taskByRun = useAPIQuery( - tenantId === null ? "" : insightsTaskByRunPath(tenantId, runId), - TaskResponseSchema, - ); - const task = taskByRun.kind === "ready" ? taskByRun.data.item : null; - const chainLookupFailed = taskByRun.kind === "error"; - const legsQuery = useAPIQuery( - tenantId === null || task === null || task.stepCount <= 1 - ? "" - : insightsTaskLegsPath(tenantId, task.id), - TaskLegsResponseSchema, - ); - const chainLegs = legsQuery.kind === "ready" ? legsQuery.data.items : null; - - return ( - - ); + return ; } /** diff --git a/apps/web/src/pages/mission-control-page.tsx b/apps/web/src/pages/mission-control-page.tsx index 49e4fa901..bdb947703 100644 --- a/apps/web/src/pages/mission-control-page.tsx +++ b/apps/web/src/pages/mission-control-page.tsx @@ -3,9 +3,9 @@ // top-level route (`/mission-control`), never `/` — `/` stays the Myra // land-hop redirect (see routes.tsx's header comment). Every panel here is // backed by a query already used elsewhere in this app (needs-you -// approvals, working tasks, top-level runs, insights activity); nothing on -// this page is invented. A panel with no honest data source renders an -// empty state naming what's missing instead of a fabricated number. +// approvals, top-level runs, insights activity); nothing on this page is +// invented. A panel with no honest data source renders an empty state +// naming what's missing instead of a fabricated number. import { Badge, @@ -32,7 +32,6 @@ import { useQueryClient } from "@tanstack/react-query"; import { formatUsd } from "@corbits/insights/client"; import type { Workbench } from "@corbits/chat-ui"; -import type { WorkingTask } from "@corbits/tasks-ui"; import { approveApproval, @@ -66,28 +65,6 @@ type InFlightRow = { readonly steps: string; }; -function taskInFlightRow(task: WorkingTask): InFlightRow { - const statusLabel = task.status === "needs-you" ? "Needs you" : "Running"; - return { - key: `task:${task.id}`, - label: task.prompt, - context: `${task.agentName} · task`, - createdAt: task.createdAt, - statusLabel, - // needs-you is react-ui's `awaiting` (the one status a person can act - // on); every other in-flight task reads `running` — both read their - // tone from react-ui's own `RUN_STATUS_TONE` rather than a hand-picked - // one. - statusTone: - task.status === "needs-you" - ? RUN_STATUS_TONE.awaiting - : RUN_STATUS_TONE.running, - // stepCount is the task's planned total; runIds is how many legs have - // actually dispatched so far — a real ratio, not an invented total. - steps: `${task.runIds.length}/${task.stepCount}`, - }; -} - function routineInFlightRow(routine: RoutineActivityItem): InFlightRow { return { key: `routine:${routine.id}`, @@ -101,21 +78,13 @@ function routineInFlightRow(routine: RoutineActivityItem): InFlightRow { }; } -/** Every task/routine this bench is actively running right now, newest - * first. Queued tasks (accepted, not yet executing) are left out — they - * are not yet "in flight." */ +/** Every routine this bench is actively running right now, newest first. */ export function computeInFlightRows( - workingTasks: readonly WorkingTask[], routines: readonly RoutineActivityItem[], ): readonly InFlightRow[] { - const rows = [ - ...workingTasks - .filter((task) => task.status !== "queued") - .map(taskInFlightRow), - ...routines - .filter((routine) => routine.status === "running") - .map(routineInFlightRow), - ]; + const rows = routines + .filter((routine) => routine.status === "running") + .map(routineInFlightRow); return rows.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt)); } @@ -261,14 +230,10 @@ export function MissionControlRoute({ : null; const inFlightRows = - activity.kind === "ready" - ? computeInFlightRows(activity.workingTasks, activity.routines) - : []; + activity.kind === "ready" ? computeInFlightRows(activity.routines) : []; const activeRunsCount = activity.kind === "ready" - ? activity.workingTasks.filter((task) => task.status === "running") - .length + - activity.routines.filter((routine) => routine.status === "running") + ? activity.routines.filter((routine) => routine.status === "running") .length : null; diff --git a/apps/web/src/pages/run-status-tone-parity.test.ts b/apps/web/src/pages/run-status-tone-parity.test.ts index b5102c1e6..4c92abe6b 100644 --- a/apps/web/src/pages/run-status-tone-parity.test.ts +++ b/apps/web/src/pages/run-status-tone-parity.test.ts @@ -22,28 +22,8 @@ import { statusTone } from "./insights-page"; import { runStatusTone } from "./routines-page"; import { computeInFlightRows } from "./mission-control-page"; import { AGENT_ROSTER_STATUS_TONE } from "./agents-page"; -import type { WorkingTask } from "@corbits/tasks-ui"; import type { RoutineActivityItem } from "../shell/routine-activity"; -function workingTask(overrides: Partial): WorkingTask { - return { - id: "tsk_1", - definitionId: "def_1", - workbenchId: null, - agentName: "Myra", - prompt: "Do the thing", - modelPreference: null, - status: "running", - runId: "run_1", - runIds: ["run_1"], - stepCount: 1, - resultMailId: null, - createdAt: new Date().toISOString(), - completedAt: null, - ...overrides, - }; -} - describe("run-status tone parity with react-ui's RUN_STATUS_TONE", () => { test("Insights' statusTone agrees with canonical for every shared status", () => { // WorkflowRunStatus ("running"/"stopped") spells these two the same way @@ -59,25 +39,13 @@ describe("run-status tone parity with react-ui's RUN_STATUS_TONE", () => { }); test("Mission Control's in-flight rows agree with canonical for every shared status", () => { - const [needsYouRow] = computeInFlightRows( - [workingTask({ id: "tsk_needs_you", status: "needs-you" })], - [], - ); - expect(needsYouRow?.statusTone).toBe(RUN_STATUS_TONE.awaiting); - - const [runningRow] = computeInFlightRows( - [workingTask({ id: "tsk_running", status: "running" })], - [], - ); - expect(runningRow?.statusTone).toBe(RUN_STATUS_TONE.running); - const routine: RoutineActivityItem = { id: "rtn_1", name: "Daily brief", status: "running", startedAt: new Date().toISOString(), }; - const [routineRow] = computeInFlightRows([], [routine]); + const [routineRow] = computeInFlightRows([routine]); expect(routineRow?.statusTone).toBe(RUN_STATUS_TONE.running); }); diff --git a/apps/web/src/pages/workbench-timeline.tsx b/apps/web/src/pages/workbench-timeline.tsx index 9cc19a4ae..8391cbe5e 100644 --- a/apps/web/src/pages/workbench-timeline.tsx +++ b/apps/web/src/pages/workbench-timeline.tsx @@ -1,9 +1,9 @@ // Per-workbench Insights view (CL-6224): one wall-clock spine merging chat -// messages, thread forks, routine runs, tasks, and approvals for a single -// workbench (== workbench, per docs/GLOSSARY.md), oldest to newest with day -// dividers. No new backend — every fetch here is an existing route this app -// already reads elsewhere (chat-ui, routines-api, tasks-ui, api.ts); the new -// work is `../workbench-timeline-merge.ts`'s pure merge, plus this render. +// messages, thread forks, routine runs, and approvals for a single workbench +// (== workbench, per docs/GLOSSARY.md), oldest to newest with day dividers. +// No new backend — every fetch here is an existing route this app already +// reads elsewhere (chat-ui, routines-api, api.ts); the new work is +// `../workbench-timeline-merge.ts`'s pure merge, plus this render. import { Badge, @@ -13,7 +13,6 @@ import { type BadgeTone, } from "@corbits/react-ui"; import { listMessages, listThreads } from "@corbits/chat-ui"; -import { listTasks } from "@corbits/tasks-ui"; import { Clock } from "@corbits/icons"; import { useMemo, useState } from "react"; @@ -34,7 +33,6 @@ import { toApprovalEvents, toMessageEvents, toRoutineRunEvents, - toTaskEvents, toThreadForkEvents, type TimelineEvent, type TimelineFilter, @@ -70,19 +68,6 @@ function routineRunTone(status: "ok" | "failed" | "running"): BadgeTone { } } -function taskStatusTone(status: string): BadgeTone { - switch (status) { - case "done": - return "success"; - case "failed": - return "danger"; - case "needs-you": - return "warning"; - default: - return "info"; - } -} - function markerClass(event: TimelineEvent): string { switch (event.kind) { case "message": @@ -97,8 +82,6 @@ function markerClass(event: TimelineEvent): string { : event.status === "failed" ? "workbench-timeline-marker-danger" : "workbench-timeline-marker-info"; - case "task": - return "workbench-timeline-marker-info"; case "approval": return "workbench-timeline-marker-warn"; } @@ -150,15 +133,6 @@ function TimelineRowBody({
); - case "task": - return ( -
- - {event.agentName} ran a task · {event.prompt} - - {event.status} -
- ); case "approval": return (
@@ -253,7 +227,7 @@ export function WorkbenchTimelineView({ } title="Nothing on this workbench's timeline yet" - description="Messages, routine runs, tasks, and approvals will show up here as they happen." + description="Messages, routine runs, and approvals will show up here as they happen." /> ); } @@ -320,7 +294,7 @@ export function WorkbenchTimelineView({ * `workbenchId` is the workbench's own id, resolved to that workbench tenant * one level up in `InsightsWorkbenchPage` (`../insights-workbench-scope.ts`) * for the tenant-scoped Insights endpoints; this component only ever reads - * messages/threads/routines/tasks/approvals off the owning bench. + * messages/threads/routines/approvals off the owning bench. */ export function WorkbenchTimelineRoute({ benchTenantId, @@ -382,13 +356,6 @@ export function WorkbenchTimelineRoute({ return new Map(entries); }, ); - const tasksQuery = useTenantQuery( - benchTenantId === null - ? ["tenant", "none", "tasks"] - : tenantKeys.tasks(benchTenantId), - benchTenantId !== null, - () => listTasks(benchTenantId as string), - ); const approvalsQuery = useAPIQuery( benchTenantId === null ? "" @@ -400,14 +367,12 @@ export function WorkbenchTimelineRoute({ messagesQuery.kind === "loading" || threadsQuery.kind === "loading" || routinesQuery.kind === "loading" || - tasksQuery.kind === "loading" || approvalsQuery.kind === "loading"; const messages = messagesQuery.kind === "ready" ? messagesQuery.data : []; const threads = threadsQuery.kind === "ready" ? threadsQuery.data : []; const routineRunsByRoutineId = routineRunsQuery.kind === "ready" ? routineRunsQuery.data : new Map(); - const tasks = tasksQuery.kind === "ready" ? tasksQuery.data : []; // needs-you carries no workbench/workbench id (a known v1 gap — see // workbench-timeline-merge.ts's toApprovalEvents), so this is every // pending approval on the owning bench, not just this workbench's own. @@ -418,7 +383,6 @@ export function WorkbenchTimelineRoute({ messages: toMessageEvents(messages), threadForks: toThreadForkEvents(threads), routineRuns: toRoutineRunEvents(routines, routineRunsByRoutineId), - tasks: toTaskEvents(tasks, workbenchId), approvals: toApprovalEvents(approvals), }); diff --git a/apps/web/src/purpose-definitions.ts b/apps/web/src/purpose-definitions.ts index 69c214907..85bc44df6 100644 --- a/apps/web/src/purpose-definitions.ts +++ b/apps/web/src/purpose-definitions.ts @@ -22,7 +22,7 @@ export function purposeDefinitions( } export type CatalogFields = { - /** The raw catalog asset name (e.g. "recurring-task") — distinct from + /** The raw catalog asset name (e.g. "workbench-digest") — distinct from * `name`, which a caller (see `listWorkflowDefinitions`) may go on to * overwrite with the friendly display name for UI rendering. A caller * that needs to recognize a *specific* known workflow (not just show diff --git a/apps/web/src/query-client.ts b/apps/web/src/query-client.ts index 4d62c6bc6..a9bdf9293 100644 --- a/apps/web/src/query-client.ts +++ b/apps/web/src/query-client.ts @@ -124,13 +124,6 @@ export const tenantKeys = { * of the literal that could drift apart. */ workbenches: (tenantId: string, kind: WorkbenchKind) => workbenchesQueryKey(tenantId, kind), - tasks: (tenantId: string) => ["tenant", tenantId, "tasks"] as const, - task: (tenantId: string, taskId: string) => - ["tenant", tenantId, "tasks", taskId] as const, - taskLegs: (tenantId: string, taskId: string) => - ["tenant", tenantId, "tasks", taskId, "legs"] as const, - taskByRun: (tenantId: string, runId: string) => - ["tenant", tenantId, "tasks", "by-run", runId] as const, topLevelRuns: (tenantId: string) => ["tenant", tenantId, "top-level-runs"] as const, /** A workbench's own timeline reads (CL-6224): `tenantId` is the owning @@ -186,15 +179,5 @@ export function pathToQueryKey(path: string): readonly unknown[] { if (artifacts?.[1] !== undefined) { return [...tenantKeys.artifacts(artifacts[1]), artifacts[2] ?? ""] as const; } - const taskLegs = /^\/api\/tenants\/([^/]+)\/tasks\/([^/]+)\/legs$/.exec(path); - if (taskLegs?.[1] !== undefined && taskLegs[2] !== undefined) { - return tenantKeys.taskLegs(taskLegs[1], taskLegs[2]); - } - const taskByRun = /^\/api\/tenants\/([^/]+)\/tasks\/by-run\/([^/]+)$/.exec( - path, - ); - if (taskByRun?.[1] !== undefined && taskByRun[2] !== undefined) { - return tenantKeys.taskByRun(taskByRun[1], taskByRun[2]); - } return ["path", path]; } diff --git a/apps/web/src/shell/bench-activity.ts b/apps/web/src/shell/bench-activity.ts index e030d8c79..e25956a14 100644 --- a/apps/web/src/shell/bench-activity.ts +++ b/apps/web/src/shell/bench-activity.ts @@ -1,14 +1,7 @@ // The sidebar's one data source: everything happening in the currently // selected bench right now. Workbenches and chats come from // `@corbits/chat-ui`'s own validated fetches; running routines come through -// the seam in `./routine-activity.ts`; the signed-in user's in-progress -// tasks come from `@corbits/tasks-ui` (`GET /tasks` is already -// creator-scoped, so every task here is already "mine" — see -// `packages/tasks/src/routes.ts`). Each task carries its own `agentName` -// (set at launch time, see `packages/tasks/src/schema.ts`), so this hook -// never has to cross-reference a definitions listing to name a row — that -// listing excludes planner-created agents (CL-6051), which would have -// left their tasks with no name to show. Notifications have no backing +// the seam in `./routine-activity.ts`. Notifications have no backing // feature in the hub yet, so they are not fetched here at all — the // column renders an honest empty state for that section instead of a // query with nowhere to point. @@ -31,8 +24,6 @@ import { listVisibleAgentDefinitions, } from "@corbits/chat-ui"; import type { Workbench, VisibleAgentDefinition } from "@corbits/chat-ui"; -import { listTasks, workingTasks } from "@corbits/tasks-ui"; -import type { WorkingTask } from "@corbits/tasks-ui"; import { tenantKeys } from "../query-client"; import { listRoutineActivity } from "./routine-activity"; @@ -48,7 +39,6 @@ export type BenchActivityQuery = readonly chats: readonly Workbench[]; readonly agents: readonly VisibleAgentDefinition[]; readonly routines: readonly RoutineActivityItem[]; - readonly workingTasks: readonly WorkingTask[]; }; function errorMessage(cause: unknown): string { @@ -95,11 +85,6 @@ export function useBenchActivity(tenantId: string | null): BenchActivityQuery { enabled, queryFn: () => listRoutineActivity(key), }); - const tasksQuery = useQuery({ - queryKey: tenantKeys.tasks(key), - enabled, - queryFn: () => listTasks(key), - }); const agentsQuery = useQuery({ queryKey: tenantKeys.visibleAgents(key), enabled, @@ -112,7 +97,6 @@ export function useBenchActivity(tenantId: string | null): BenchActivityQuery { workbenchesQuery, chatsQuery, routinesQuery, - tasksQuery, agentsQuery, ]) { if (query.isError) @@ -122,7 +106,6 @@ export function useBenchActivity(tenantId: string | null): BenchActivityQuery { workbenchesQuery.data === undefined || chatsQuery.data === undefined || routinesQuery.data === undefined || - tasksQuery.data === undefined || agentsQuery.data === undefined ) { return { kind: "loading" }; @@ -134,6 +117,5 @@ export function useBenchActivity(tenantId: string | null): BenchActivityQuery { chats: chatsQuery.data, agents: agentsQuery.data, routines: routinesQuery.data, - workingTasks: workingTasks(tasksQuery.data), }; } diff --git a/apps/web/src/shell/workbench-list.tsx b/apps/web/src/shell/workbench-list.tsx index 722f4d154..9c86ea1e9 100644 --- a/apps/web/src/shell/workbench-list.tsx +++ b/apps/web/src/shell/workbench-list.tsx @@ -1,9 +1,7 @@ // The sidebar's one list: every workbench (an agent conversation, each its // own tenancy under the hood) as a flat run of rows — no kind sections, no // per-page variants. Pinned rows float to the top; everything else keeps -// the order the platform returns. The "Working" group above the rows is the -// signed-in user's running tasks (spawn-and-return; selecting one opens its -// workbench, or its run in Insights when it has none), not a workbench kind. +// the order the platform returns. import { Badge, @@ -24,7 +22,6 @@ import { patchWorkbenchSettings, } from "@corbits/chat-ui"; import type { Workbench } from "@corbits/chat-ui"; -import { WorkingTaskRow } from "@corbits/tasks-ui"; import { useQueryClient } from "@tanstack/react-query"; import { ChatCircle, DotsThree, Hash, MagnifyingGlass } from "@corbits/icons"; import { useEffect, useState } from "react"; @@ -402,30 +399,10 @@ export function WorkbenchList({ } const all = buildSidebarRows(activity.workbenches, activity.chats); - const workingGroup = - activity.workingTasks.length > 0 ? ( -
-

Working

- {activity.workingTasks.map((task) => ( - - onNavigate( - task.workbenchId !== null - ? workbenchPath(task.workbenchId) - : `/insights/runs/${encodeURIComponent(task.runId)}`, - ) - } - /> - ))} -
- ) : null; if (all.length === 0) { return (
- {workingGroup}

Workbenches

@@ -449,7 +426,6 @@ export function WorkbenchList({ return (
- {workingGroup}