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
38 changes: 32 additions & 6 deletions docs/contributing/packages-and-manifests.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,13 @@ The top-level saved identity is the package.

### Dynamic current-version invocation

Package runtime contexts also expose `packages.invoke` from `kody:runtime`:
Package runtime contexts also expose `packages.check`, `packages.invoke`, and
`packages.invokeChecked` from `kody:runtime`:

```ts
import { packages } from 'kody:runtime'

await packages.invoke({
await packages.invokeChecked({
kodyId: 'discord-general-chat',
exportName: './handle-discord-message-created',
params: { event },
Expand All @@ -108,14 +109,39 @@ package-invocation bearer tokens for this flow.

Use dynamic invocation for runtime dispatch surfaces that must pick up the
target package's current published bundle, such as event subscribers, workflows,
and agents. Continue to use static `kody:@scope/package/export` imports for
library-like dependencies where the caller should keep the dependency bundle it
was published with.
and agents. Prefer `packages.invokeChecked` for new dynamic calls so Kody first
checks that the current package and export exist, params are a JSON object, and
the current contract metadata can be surfaced to the caller. Use
`packages.check` directly when a caller wants to inspect the current contract or
warnings before deciding whether to invoke:

```ts
const check = await packages.check({
kodyId: 'discord-general-chat',
exportName: './handle-discord-message-created',
params: { event, dryRun: true },
})

if (!check.ok) throw new Error(check.message)
const result = await packages.invoke(check.invoke)
```

Continue to use static `kody:@scope/package/export` imports for library-like
dependencies where the caller should keep the dependency bundle it was published
with. Use bare `packages.invoke` only after a successful `packages.check` or
when the caller intentionally accepts direct runtime failure.

`packages.check` returns the current package id/kody id/name, source id,
published commit, normalized export name, runtime target, and available
JSDoc/type definition. It also returns warnings when validation is weak. Today
package exports do not publish machine-readable params schemas, so Kody cannot
validate field-level params shape beyond requiring `params` to be a JSON object.

`packages.invoke` returns the target export's unwrapped result. Non-2xx package
invocation responses reject with an error whose message starts with the
underlying code, for example `[package_not_found] ...` or
`[export_not_found] ...`.
`[export_not_found] ...`. `packages.invokeChecked` throws before invoking when
the check fails.

Idempotency:

Expand Down
5 changes: 4 additions & 1 deletion docs/use/execute.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ helpers are runtime exports:
code when you need package metadata; it is **`null`** for ad hoc execute calls
- use **`import { packages } from 'kody:runtime'`** only inside saved package
runtime contexts when you need dynamic current-version invocation through
`packages.invoke(...)`; it is **`null`** for ad hoc execute calls
`packages.check(...)`, `packages.invoke(...)`, or
`packages.invokeChecked(...)`; it is **`null`** for ad hoc execute calls.
Prefer `invokeChecked` unless you already called `check` and are passing
`check.invoke` to `invoke`
- use **`import { serviceContext } from 'kody:runtime'`** inside package service
code when you need the current service identity; it is **`null`** outside
package service runs
Expand Down
67 changes: 58 additions & 9 deletions docs/use/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,64 @@ const result = await packages.invoke({
})
```

Use `packages.invoke` for event subscribers, workflow dispatchers, agents, and
other runtime fan-out where the caller should pick up the target package's
current published export. The call resolves the target package at runtime, so
republishing `discord-general-chat` changes what `discord-gateway` observes
without republishing `discord-gateway`.
Use `packages.invokeChecked` for event subscribers, workflow dispatchers,
agents, and other runtime fan-out where the caller should pick up the target
package's current published export and wants Kody to validate the current
runtime contract before invoking it. The check resolves the target package at
runtime, so republishing `discord-general-chat` changes what `discord-gateway`
observes without republishing `discord-gateway`.

Use static `kody:@scope/package/export` imports for library-like dependencies
where bundling a published dependency snapshot with the caller is desired.

`packages.invoke` returns the target export's unwrapped return value. If the
target package, export, or execution fails, the promise rejects with an error
that includes the package invocation error code in the message.
Use `packages.check` when the caller wants to inspect the current contract
before deciding whether to invoke:

```ts
import { packages } from 'kody:runtime'

const check = await packages.check({
kodyId: 'discord-general-chat',
exportName: './handle-discord-message-created',
params: { event, dryRun: true },
})

if (!check.ok) throw new Error(check.message)
console.log(check.contract)

const result = await packages.invoke(check.invoke)
```

For the common check-then-invoke flow, use the combined helper:

```ts
import { packages } from 'kody:runtime'

const result = await packages.invokeChecked({
kodyId: 'discord-general-chat',
exportName: './handle-discord-message-created',
params: { event, dryRun: true },
})
```

Use bare `packages.invoke` only when the caller has already checked the contract
or intentionally wants the older direct invoke behavior. Use static
`kody:@scope/package/export` imports for library-like dependencies where
bundling the currently published dependency with the caller is desired.

`packages.check` returns `ok: false` with `message` and `problems` when the
package, export, or params are invalid. On success it returns `contract`
metadata, including package id/kody id/name, source id, published commit,
normalized export name, runtime target, available JSDoc/type definition, and
warnings on `check.contract.warnings`. Those warnings are important: Kody
currently has JSDoc/type metadata but no machine-readable params schema for
package exports, so params are only validated as a JSON object.

`packages.invoke` and `packages.invokeChecked` return the target export's
unwrapped return value. If execution fails, the promise rejects with an error
that includes the package invocation error code in the message. If the
pre-invoke check fails, `packages.invokeChecked` rejects before invoking the
target export.

The primary identifier is `kodyId`; `kody_id`, `packageId`, and `package_id` are
accepted aliases for compatibility. `exportName` is required, and `export_name`
Expand All @@ -136,7 +182,10 @@ Ad hoc MCP `execute` code, including saved package modules imported from that
execute call, does not get a package runtime context. In those runs
`import { packages } from 'kody:runtime'` returns `null`; enter the package
through package invocation, a package subscription, a package job, service, or
app when the code needs `packages.invoke`.
app when the code needs `packages.check`, `packages.invoke`, or
`packages.invokeChecked`. Inside package runtime code, the equivalent codemode
helpers are `codemode.package_invoke_check`, `codemode.package_invoke`, and
`codemode.package_invoke_checked`.

If `idempotencyKey` is omitted, Kody generates one. In package invocations that
already have a parent idempotency key, the generated key is deterministic for
Expand Down
42 changes: 42 additions & 0 deletions packages/worker/src/mcp/run-codemode-registry.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1513,13 +1513,43 @@ test('runBundledModuleWithRegistry injects package invocation helper when provid
undefined,
{
packageInvokeTools: {
check: async (input) => ({
ok: true,
invoke: input as {
kodyId: string
exportName: string
},
contract: {
packageId: 'pkg-discord-general-chat',
kodyId: 'discord-general-chat',
name: '@kentcdodds/discord-general-chat',
sourceId: 'source-discord-general-chat',
publishedCommit: 'commit-1',
exportName: './handle-discord-message-created',
runtimeTarget: 'src/handle-discord-message-created.ts',
warnings: [],
},
}),
invoke: async (input) => ({ ok: true, input }),
invokeChecked: async (input) => ({ ok: true, input }),
},
},
)

expect(result.result).toBe('ok')
expect(providerFns).not.toBeNull()
await expect(
providerFns?.package_invoke_check({
kodyId: 'discord-general-chat',
exportName: './handle-discord-message-created',
}),
).resolves.toMatchObject({
ok: true,
invoke: {
kodyId: 'discord-general-chat',
exportName: './handle-discord-message-created',
},
})
await expect(
providerFns?.package_invoke({
kodyId: 'discord-general-chat',
Expand All @@ -1532,6 +1562,18 @@ test('runBundledModuleWithRegistry injects package invocation helper when provid
exportName: './handle-discord-message-created',
},
})
await expect(
providerFns?.package_invoke_checked({
kodyId: 'discord-general-chat',
exportName: './handle-discord-message-created',
}),
).resolves.toEqual({
ok: true,
input: {
kodyId: 'discord-general-chat',
exportName: './handle-discord-message-created',
},
})
} finally {
createExecuteExecutorSpy.mockRestore()
getRegistrySpy.mockRestore()
Expand Down
45 changes: 45 additions & 0 deletions packages/worker/src/mcp/run-codemode-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,45 @@ type EmailToolOptions = {

export type PackageInvokeInput = Record<string, unknown>

export type PackageInvokeNormalizedInput = {
kodyId?: string
packageId?: string
exportName: string
params?: Record<string, unknown>
idempotencyKey?: string
topic?: string
}

export type PackageInvokeContract = {
packageId: string
kodyId: string
name: string
sourceId: string
publishedCommit: string | null
exportName: string
runtimeTarget: string | null
description?: string | null
typeDefinition?: string | null
warnings: Array<string>
}

export type PackageInvokeCheckResult =
| {
ok: true
invoke: PackageInvokeNormalizedInput
contract: PackageInvokeContract
}
| {
ok: false
message: string
problems: Array<string>
contract?: Partial<PackageInvokeContract>
}

export type PackageInvokeTools = {
invoke: (input: PackageInvokeInput) => Promise<unknown>
check: (input: PackageInvokeInput) => Promise<PackageInvokeCheckResult>
invokeChecked: (input: PackageInvokeInput) => Promise<unknown>
}

export type PackageWorkflowTools = {
Expand Down Expand Up @@ -246,7 +283,9 @@ const workflows = {
function createPackagesHelperPrelude() {
return `
const packages = {
check: async (input) => await codemode.package_invoke_check(input ?? {}),
invoke: async (input) => await codemode.package_invoke(input ?? {}),
invokeChecked: async (input) => await codemode.package_invoke_checked(input ?? {}),
};
`.trim()
}
Expand Down Expand Up @@ -419,8 +458,14 @@ export async function buildCodemodeFns(
const packageInvokeTools = options?.packageInvokeTools
const packageInvokeCodemodeTools: AdditionalCodemodeTools = packageInvokeTools
? {
package_invoke_check: async (args: unknown) =>
await packageInvokeTools.check((args ?? {}) as PackageInvokeInput),
package_invoke: async (args: unknown) =>
await packageInvokeTools.invoke((args ?? {}) as PackageInvokeInput),
package_invoke_checked: async (args: unknown) =>
await packageInvokeTools.invokeChecked(
(args ?? {}) as PackageInvokeInput,
),
}
: {}
assertNoCapabilityCollisions(capabilityMap, packageInvokeCodemodeTools)
Expand Down
2 changes: 1 addition & 1 deletion packages/worker/src/mcp/server-instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ search

execute
- Single ESM module string with a default export such as \`export default async function main(input = {}) { ... }\`; \`params\` are passed as the first argument. Import runtime APIs from \`kody:runtime\`. Example: \`import { codemode, refreshAccessToken, createAuthenticatedFetch, workflows } from 'kody:runtime'\`. \`workflows.create\` can queue either inline \`code\` or a saved-package \`exportName\` through the shared Workflow hub. Prefer one \`execute\` when the plan is clear. Full rules for \`fetch\`, placeholders, \`secret_list\` / \`value_get\`, and \`x-kody-secret\`: see the \`execute\` tool description.
- Cross-package imports use specifiers such as \`kody:@scope/my-package/export-name\`. Saved package names must be scoped (\`@scope/<leaf>\`) and the leaf segment must match \`kody.id\`. Package jobs are owned by packages, ad hoc jobs can be scheduled with \`job_schedule\`, and package apps are optional package surfaces.
- Cross-package imports use specifiers such as \`kody:@scope/my-package/export-name\`. For dynamic current-version calls inside package runtime code, prefer \`packages.invokeChecked({ kodyId, exportName, params })\` or \`packages.check(...)\` followed by \`packages.invoke(check.invoke)\`; use static imports for library-like bundled snapshots. Saved package names must be scoped (\`@scope/<leaf>\`) and the leaf segment must match \`kody.id\`. Package jobs are owned by packages, ad hoc jobs can be scheduled with \`job_schedule\`, and package apps are optional package surfaces.
- Official how-to guides from the Kody repo: if a requested package or workflow depends on a third-party integration, secrets, or OAuth, call \`kody_official_guide\` with \`guide: "integration_bootstrap"\` before building the package. Then load the relevant setup guide: \`oauth\` for standard third-party OAuth (\`/connect/oauth\`), \`connect_secret\` for secret collection, and \`secret_backed_integration\` for the default non-OAuth secret-backed recipe after bootstrap. If unsure, \`search\` for this capability and load the right guide before implementing.
- Do not save or present an auth-dependent package as complete until \`search\` shows the required integration or secret reference exists and a minimal authenticated \`execute\` smoke test succeeds.

Expand Down
2 changes: 1 addition & 1 deletion packages/worker/src/mcp/tools/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Sandbox surface:
- \`import { workflows } from 'kody:runtime'\` for durable Cloudflare Workflows. \`workflows.create\` accepts either inline \`code\` or a saved-package \`exportName\`; use \`workflow_list\` to inspect recent runs.
- Optional \`params\` are passed as the first argument to the module default export. Prefer \`export default async function main(input = {}) { ... }\`; pass \`input\` to shared helpers explicitly.
- \`import { packageContext } from 'kody:runtime'\` in saved package code when you need package metadata; it is \`null\` for ad hoc execute calls.
- \`import { packages } from 'kody:runtime'\` exposes \`packages.invoke(...)\` only in saved package runtime contexts; it is \`null\` for ad hoc execute calls.
- \`import { packages } from 'kody:runtime'\` exposes \`packages.check(...)\`, \`packages.invoke(...)\`, and \`packages.invokeChecked(...)\` only in saved package runtime contexts; it is \`null\` for ad hoc execute calls. Prefer \`invokeChecked\` for dynamic current-version package calls unless you already called \`check\` and pass \`check.invoke\` to \`invoke\`.
- \`fetch(...)\` is the host-provided network global; \`{{secret:name}}\` / \`{{secret:name|scope=user}}\` work in URL, headers, or body on approved hosts only.
- Fields marked \`x-kody-secret: true\` accept the same placeholder form; respect per-secret allowed-capability lists.
- Placeholders are not general string interpolation (they do not resolve in arbitrary return values).
Expand Down
Loading
Loading