Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 8 additions & 85 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ import {
} from "@corbits/webhook-triggers";
import {
deliveryWorkbenchRequiredForWorkflowName,
isAutomatableWorkflowName,
isConversationalWorkflowName,
validateTriggerFieldsAtCreate,
webhookTriggerName,
Expand All @@ -201,18 +200,14 @@ import {
WORKFLOW_SOURCE_ENTRY,
} from "@corbits/workflows";
import {
createDrizzleDraftStore,
createDrizzleRoutineStore,
createMyraRoutineDrafting,
createRoutineRoutes,
createRoutineTargetRoutes,
createWorkflowRoutineRoutes,
listLaunchableDefinitions,
listRoutineTargets,
resolveLaunchableDefinition,
routine as routineTable,
routineRun as routineRunTable,
type RoutineDraftInventoryWorkflow,
} from "@corbits/routines";
import {
createSidecarProvisioner as createE2BSidecarProvisioner,
Expand Down Expand Up @@ -2754,8 +2749,8 @@ export async function createHub(config: HubConfig) {
bus: mailboxBus,
});

// Shared `FoldedRunsDeps` for every one-shot Myra prompt below (routine
// drafting, agent-definition drafting): a real one-shot inference call
// Shared `FoldedRunsDeps` for every one-shot Myra prompt below
// (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.
Expand Down Expand Up @@ -2826,7 +2821,6 @@ export async function createHub(config: HubConfig) {
// real status instead of a bare run id.
const routineGrantStore = createGrantStore(db);
const routineStore = createDrizzleRoutineStore(db);
const routineDraftStore = createDrizzleDraftStore(db);
// The honest end-to-end delivery-destination rule: a workflow that
// never posts to a workbench (e.g. recurring-task, always delivering
// to its creator's Inbox — see @corbits/workflow-catalog's
Expand Down Expand Up @@ -2905,51 +2899,6 @@ export async function createHub(config: HubConfig) {
return { ok: true };
}

/**
* The routine-drafting inventory's workflow half: every launchable
* definition (CL-7351's `listLaunchableDefinitions` — deployed,
* frozen, `authored`) in the tenant whose catalog entry is
* `automatable`, carrying the exact `triggerFields`/`deliveryMode`
* Myra's drafted trigger input is checked against (`@corbits/routines`'
* `validateRoutineDraftReplyAgainstInventory`). Sources its candidate
* rows from the one canonical launchable-definitions query
* (CL-7359) rather than a second, independently-filtered
* `workflowDefinition` scan. Mirrors `listMyraConversationalAgents`
* below in shape, scoped to automatable rather than conversational
* definitions.
*/
async function listAutomatableWorkflowsForDraftInventory(
tenantId: string,
): Promise<readonly RoutineDraftInventoryWorkflow[]> {
const candidates = await listLaunchableDefinitions(db, tenantId);
const out: RoutineDraftInventoryWorkflow[] = [];
for (const candidate of candidates) {
if (!isAutomatableWorkflowName(candidate.name)) continue;
const entry = workflowCatalogEntry(candidate.name);
if (entry === undefined) continue;
const workflow = {
definitionAssetId: candidate.definitionAssetId,
assetName: candidate.name,
displayName: workflowDisplayName(candidate.name, candidate.description),
deliveryMode: entry.deliveryMode,
triggerFields: entry.triggerFields ?? [],
};
out.push(
candidate.description !== null
? { ...workflow, description: candidate.description }
: workflow,
);
}
return out;
}

// 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({
db,
sessionService,
Expand Down Expand Up @@ -3015,41 +2964,14 @@ export async function createHub(config: HubConfig) {
)
.then(() => undefined),
};
// Routines routes own their `/routines` and `/routine-drafts` prefixes, so
// mount at the tenant root (same pattern as a package that ships absolute
// Routines routes own their `/routines` prefix, so mount at the
// tenant root (same pattern as a package that ships absolute
// resource paths) rather than under a second `/routines` segment.
app.route(
TENANT_PREFIX,
createRoutineRoutes({
store: routineStore,
drafts: routineDraftStore,
workbenchNotice: routineWorkbenchNotice,
// Myra-backed drafting (CL-5917): a real one-shot inference call,
// 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) =>
resolveMyraDefinitionIdFromDb(db, tenantId),
runner: {
run: (runnerInput) =>
runOneShotFoldedPrompt(
{
foldedRuns: oneShotFoldedRunsDeps,
events: sidecarRouter.events,
cryptoProviders: routineDraftingCryptoProviders,
undeploy: (address, reason) =>
sidecarRouter.sendAgentUndeploy(address, reason),
},
runnerInput,
),
},
inventorySources: {
listAutomatableWorkflows: listAutomatableWorkflowsForDraftInventory,
listTaskableAgents: listMyraConversationalAgents,
},
}),
launcher: routineLauncher,
requireGrant: createRequireGrant({
grantStore: routineGrantStore,
Expand Down Expand Up @@ -3292,9 +3214,10 @@ export async function createHub(config: HubConfig) {
listModels: listMyraModels,
};

// 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.
// A separate `CryptoProviderCache` from `foldedRunCryptoProviders`
// above: an agent-definition drafting one-shot run's instance id has
// nothing to do with a folded run's, so a separate cache keeps them
// from ever contending over the same key space.
const agentDefinitionDraftingCryptoProviders = createCryptoProviderCache();

// The create-agent panel's "Describe" step (CL-6074): a real one-shot
Expand Down
55 changes: 0 additions & 55 deletions apps/web/src/routines-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,11 @@ import {
} from "@corbits/api-query";
import {
Routine,
RoutineDraft,
RoutineRun,
RoutinesResponse,
RoutineRunsResponse,
RoutineTargetsResponse,
routineCreatedToast,
routineDraftApprovePath,
routineDraftDiscardPath,
routineDraftsPath,
routinePath,
routineRunNowPath,
routineRunStartedToast,
Expand All @@ -41,19 +37,14 @@ import {
routineTargetsPath,
} from "@corbits/routines/client";
import type {
CreateDraftInput,
CreateRoutineInput,
RoutineTarget,
UpdateRoutineInput,
} from "@corbits/routines/client";

export {
DraftedStep,
suggestRoutineNameFromPrompt,
type CreateDraftInput,
type CreateRoutineInput,
type Routine,
type RoutineDraft,
type RoutineRun,
type RoutineTarget,
type RoutineTargetKind,
Expand Down Expand Up @@ -174,52 +165,6 @@ export function listRoutineRuns(
);
}

export function createRoutineDraft(
tenantId: string,
input: CreateDraftInput,
): Promise<RoutineDraft> {
return request(routineDraftsPath(tenantId), RoutineDraft, {
method: "POST",
body: JSON.stringify(input),
});
}

export function listRoutineDrafts(
tenantId: string,
): Promise<readonly RoutineDraft[]> {
return request(
routineDraftsPath(tenantId),
type({ items: RoutineDraft.array() }),
).then((page) => page.items);
}

export function approveRoutineDraft(
tenantId: string,
id: string,
definitionAssetId?: string,
): Promise<{ draft: RoutineDraft; routine: Routine }> {
return request(
routineDraftApprovePath(tenantId, id),
type({ draft: RoutineDraft, routine: Routine }),
{
method: "POST",
body: JSON.stringify(
definitionAssetId !== undefined ? { definitionAssetId } : {},
),
},
);
}

export function discardRoutineDraft(
tenantId: string,
id: string,
): Promise<RoutineDraft> {
return request(routineDraftDiscardPath(tenantId, id), RoutineDraft, {
method: "POST",
body: JSON.stringify({}),
});
}

/** One page of definitions the signed-in principal may target from a
* routine, ordered by name; pass the previous page's `nextCursor` to
* continue. */
Expand Down
18 changes: 18 additions & 0 deletions docs/workflow-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,24 @@ re-exports `@corbits/routines/client`.
No compatibility shim, feature flag, or dual-write period accompanies any
of the deletions above.

## Deleted in CL-7375

The routine draft/review state machine — `POST /routine-drafts` (create),
`GET /routine-drafts`/`GET /routine-drafts/:id` (review), `POST
/routine-drafts/:id/approve`, and `POST /routine-drafts/:id/discard`, the
`routine_draft` table, `@corbits/routines`' `drafts.ts` and
`myra-drafting.ts`, and `suggestRoutineNameFromPrompt`. Myra creates a
routine only through `GET .../workflows/targets` → `routine_create` /
`routine_update` (`@corbits/routines-tools`'s `tool.ts`) — the same
tool-call surface a person's own create/retarget request goes through.
Neither carries an `approval: "ask"` key (they grant no credentials and
touch nothing external — only `routine_run_now` does), so the confirm
seam here is `definitionAssetId` being a required input Myra must name
explicitly, never auto-resolved from a name inside the tool. There was
never a second, review-first path for her to fall back to; `routine_draft`
is dropped by a migration (`0007_drop_routine_draft`), not left as inert
dead weight.

## What is not native, and stays in Workbench

Checked against upstream origin/main `d187e327`: Interchange has no
Expand Down
8 changes: 6 additions & 2 deletions packages/routines/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ scheduled fire) goes through `@corbits/folded-runs`, the same launch core
layer, including dead-letter bookkeeping.
- `src/client.ts` — the browser-safe subpath: wire schemas and path
builders only, no `drizzle-orm`/`postgres`/`@intx/hub-api` imports.
- `src/drafts.ts` / `src/myra-drafting.ts` — routine-draft creation and
Myra-assisted drafting flow.
- `src/migrations.ts` — this package's own `routine_migrations` ledger.

Myra creates routines only through `routine_targets` →
`routine_create`/`routine_update` (`@corbits/routines-tools`), the same
tool-call surface a person's own create/retarget request goes through —
never a separate draft/review state machine (deleted, CL-7375). See
docs/workflow-model.md.

## Routine targets follow the latest deployed asset

A routine stores `definitionAssetId`, not a pinned `workflow_definition`
Expand Down
37 changes: 0 additions & 37 deletions packages/routines/src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@ import { type } from "arktype";

import {
Routine,
RoutineDraft,
routineCreatedToast,
routineDraftApprovePath,
routineDraftDiscardPath,
routineDraftPath,
routineDraftsPath,
routinePath,
routineRunNowPath,
routineRunStartedToast,
Expand Down Expand Up @@ -79,19 +74,6 @@ describe("routine path builders", () => {
"/api/tenants/t1/routines/r1/runs",
);
});

test("build tenant-scoped routine-draft paths", () => {
expect(routineDraftsPath("t1")).toBe("/api/tenants/t1/routine-drafts");
expect(routineDraftPath("t1", "d1")).toBe(
"/api/tenants/t1/routine-drafts/d1",
);
expect(routineDraftApprovePath("t1", "d1")).toBe(
"/api/tenants/t1/routine-drafts/d1/approve",
);
expect(routineDraftDiscardPath("t1", "d1")).toBe(
"/api/tenants/t1/routine-drafts/d1/discard",
);
});
});

describe("wire schemas", () => {
Expand Down Expand Up @@ -165,23 +147,4 @@ describe("wire schemas", () => {
});
expect(out instanceof type.errors).toBe(false);
});

test("RoutineDraft parses a drafted proposal", () => {
const out = RoutineDraft({
id: "d1",
prompt: "Summarize every morning",
status: "draft",
proposedSteps: [{ title: "Summarize inbox" }],
proposedTrigger: { kind: "daily", hour: 9, minute: 0 },
proposedName: "Morning brief",
definitionAssetId: null,
deliveryWorkbenchId: "ch_1",
scope: "personal",
autonomy: null,
approvedRoutineId: null,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
expect(out instanceof type.errors).toBe(false);
});
});
Loading
Loading