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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Breaking:** `createResidentDistiller` runs on a `mail` trigger (CL-8798).
`CreateResidentDistillerOpts.cron` is replaced by a required `mailTo`, the
address the workflow's run is triggered at, and
`RESIDENT_DISTILLER_CRON_DEFAULT` is removed. The `schedule` trigger was
reserved with nothing behind it in `@intx/workflow`, so the distiller never
ticked, and 0.4.0 rejects it at `defineWorkflow`. Ticking is the host's job:
mail `mailTo` on a schedule, e.g. from `@corbits/cron`.
- `@intx/*` dependencies move to 0.4.0 (CL-8797).
- **Product narrative:** default path is **add → ingest elements → process**
(one host pipeline). Pull feed + `createResidentDistiller` are optional
multi-writer / backfill process helpers, not the primary ingest story.
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ a tenant-session request:

```ts
import { Hono } from "hono";
import type { TenantEnv } from "@intx/hub-api";
import {
mountWorkflowMemory,
type Memory,
Expand Down Expand Up @@ -137,6 +138,33 @@ docs. Details: [`docs/AUTHZ-DOCUMENT-ACCESS.md`](docs/AUTHZ-DOCUMENT-ACCESS.md).
The resident distiller (`createResidentDistiller` / `runDistillTick`) is at
`@corbits/memory/distiller`.

### Resident distiller

`createResidentDistiller` returns a mail-triggered workflow + agent, preloaded
with the memory sidecar tools. It never ships a scheduler — the host owns
ticking:

```ts
import { createResidentDistiller } from "@corbits/memory/distiller";

const { workflow, agent } = createResidentDistiller({
mailTo: "resident-distiller@tenant.example.com",
inference: { sources: [{ provider: "openai", model: "gpt-4.1-mini" }] },
});
// deploy `workflow` (and `agent`) with the host's workflow-deploy; the run
// fires on the first inbound mail at `mailTo` and stays live for the host
// to keep addressing.
```

Ticking is a host concern, not this package's: point a scheduler at
`mailTo` on whatever cadence you want. If the host already runs
`@corbits/cron`, wire a schedule whose delivery mails `mailTo` — the same
pattern a cron-ticked workflow uses everywhere on Interchange: the ticker
polls for due rows and hands each one to the host's mail transport, so a
mail-triggered workflow is ticked by addressing it directly. No import of
`@corbits/cron` is required here; see its own README for `mountCron` /
`createCronTicker` wiring.

### Lower-level: in-process calls, no HTTP

`app` is optional. Passing only `config` builds the plane without
Expand Down
44 changes: 22 additions & 22 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion docs/DISTILLER.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ cursor = result.nextCursor; // persist
Inference is **always injected** (`distill` callback or host agent sources).
The package never embeds a model.

## Optional: schedule workflow scaffold (`createResidentDistiller`)
## Optional: mail-triggered workflow scaffold (`createResidentDistiller`)

Scaffold for hosts that still want a deployed agent with memory tools +
system prompt (loop-safety, access-tag copy). Prefer wiring **process next to
Expand All @@ -76,13 +76,17 @@ add** in your ingest workflow; use this for backfill-style residency only.
import { createResidentDistiller } from "@corbits/memory/distiller";

const { workflow, generatorAgentId } = createResidentDistiller({
mailTo: "resident-distiller@tenant.example.com",
inference: {
sources: [{ provider: "openai", model: "gpt-4.1-mini" }],
},
});
// Deploy only if you need a multi-writer pull consumer — not default ingest.
```

The run fires on mail to `mailTo`. The host decides when: mail that address
on a schedule, e.g. from `@corbits/cron`. This package owns no clock.

## Substrate (plane)

| Piece | Where |
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@
"test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-reporter=text ./src"
},
"dependencies": {
"@intx/agent": "^0.3.0",
"@intx/authz": "^0.3.0",
"@intx/hub-api": "^0.3.0",
"@intx/log": "^0.3.0",
"@intx/types": "^0.3.0",
"@intx/workflow": "^0.3.0",
"@intx/agent": "^0.4.0",
"@intx/authz": "^0.4.0",
"@intx/hub-api": "^0.4.0",
"@intx/log": "^0.4.0",
"@intx/types": "^0.4.0",
"@intx/workflow": "^0.4.0",
"arktype": "^2.1.29",
"drizzle-orm": "^0.45.1",
"hono": "^4.9.0",
Expand Down
3 changes: 0 additions & 3 deletions src/distiller/constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
/** Stable generator id for the resident distiller — use with feed excludeGenerator. */
export const RESIDENT_DISTILLER_AGENT_ID = "resident-distiller";

/** Default schedule: every 5 minutes. */
export const RESIDENT_DISTILLER_CRON_DEFAULT = "*/5 * * * *";

/** Default workflow id when host does not override. */
export const RESIDENT_DISTILLER_WORKFLOW_ID = "resident-memory-distiller";
4 changes: 2 additions & 2 deletions src/distiller/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* Two ways to run:
*
* 1. **Workflow** (recommended on Interchange):
* `createResidentDistiller({ inference })` → deploy the workflow.
* `createResidentDistiller({ mailTo, inference })` → deploy the
* workflow, then have the host mail `mailTo` on a schedule to tick it.
*
* 2. **Imperative tick** (any scheduler):
* `runDistillTick({ client, distill, after })` with your model in `distill`.
Expand All @@ -14,7 +15,6 @@
*/
export {
RESIDENT_DISTILLER_AGENT_ID,
RESIDENT_DISTILLER_CRON_DEFAULT,
RESIDENT_DISTILLER_WORKFLOW_ID,
} from "./constants.ts";

Expand Down
21 changes: 15 additions & 6 deletions src/distiller/workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { SIDECAR_BUNDLE_ID } from "../sidecar-bundle.ts";
import { createResidentDistiller } from "./workflow.ts";

describe("createResidentDistiller", () => {
it("returns a schedule workflow with memory tools on the agent", () => {
it("returns a mail-triggered workflow with memory tools on the agent", () => {
const { workflow, agent, generatorAgentId } = createResidentDistiller({
mailTo: "resident-distiller@tenant.example.com",
inference: {
sources: [{ provider: "openai", model: "gpt-4.1-mini" }],
},
Expand All @@ -18,7 +19,7 @@ describe("createResidentDistiller", () => {
expect(generatorAgentId).toBe(RESIDENT_DISTILLER_AGENT_ID);
expect(workflow.id).toBe(RESIDENT_DISTILLER_WORKFLOW_ID);
expect(workflow.triggers).toEqual([
{ type: "schedule", cron: "*/5 * * * *" },
{ type: "mail", to: "resident-distiller@tenant.example.com" },
]);
expect(agent.id).toBe(RESIDENT_DISTILLER_AGENT_ID);
// One factory: the memory sidecar bundle carries every memory tool.
Expand All @@ -29,22 +30,30 @@ describe("createResidentDistiller", () => {
expect(agent.systemPrompt).toContain(RESIDENT_DISTILLER_AGENT_ID);
});

it("allows cron and id overrides", () => {
it("allows mailTo and id overrides", () => {
const { workflow, generatorAgentId, agent } = createResidentDistiller({
id: "my-distiller",
agentId: "my-agent",
cron: "0 * * * *",
mailTo: "my-agent@acme.example.com",
inference: {
sources: [{ provider: "openai", model: "gpt-4.1-mini" }],
},
});
expect(workflow.id).toBe("my-distiller");
expect(generatorAgentId).toBe("my-agent");
expect(workflow.triggers[0]).toEqual({
type: "schedule",
cron: "0 * * * *",
type: "mail",
to: "my-agent@acme.example.com",
});
expect(agent.systemPrompt).toContain("my-agent");
expect(agent.systemPrompt).not.toContain(RESIDENT_DISTILLER_AGENT_ID);
});

it("does not carry a cron/schedule field in its public options", () => {
const opts: import("./workflow.ts").CreateResidentDistillerOpts = {
mailTo: "resident-distiller@tenant.example.com",
inference: { sources: [{ provider: "openai", model: "gpt-4.1-mini" }] },
};
expect("cron" in opts).toBe(false);
});
});
Loading
Loading