Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4b8c2a7
Add package services runtime
cursoragent Apr 24, 2026
8147b28
Bind package services in preview and test
cursoragent Apr 24, 2026
05e040d
Polish package services review feedback
cursoragent Apr 24, 2026
28f7fb8
Fix oauth workers env for package services
cursoragent Apr 24, 2026
8397a00
Tighten package services runtime parity
cursoragent Apr 24, 2026
f3d0d47
Schedule package service auto-start alarms
cursoragent Apr 24, 2026
21a9553
Tighten package service capability inputs
cursoragent Apr 24, 2026
b2c5bb3
Make package services gateway-friendly
cursoragent Apr 25, 2026
5c894ad
Add package service timeout controls
cursoragent Apr 25, 2026
97404a3
Export service helper from kody runtime
cursoragent Apr 25, 2026
cf3b668
Add Discord gateway package guide
cursoragent Apr 25, 2026
1bf4480
Keep service runtime helpers in module execution
cursoragent Apr 25, 2026
a3a31bd
Polish package service runtime flow
cursoragent Apr 25, 2026
02503bc
Recover package services after DO restart
cursoragent Apr 25, 2026
9f66ded
Preserve explicit package service stops
cursoragent Apr 25, 2026
7c6611c
Harden package service alarm behavior
cursoragent Apr 25, 2026
f9c49d4
Polish package service RPC handling
cursoragent Apr 25, 2026
54641bc
Generalize package service examples
cursoragent Apr 25, 2026
4790002
Tighten package service helper semantics
cursoragent Apr 25, 2026
5428196
Retry MCP test server port binding
cursoragent Apr 25, 2026
fdf5f67
Clear stale package service stop requests
cursoragent Apr 25, 2026
55301df
Refine package service lifecycle edges
cursoragent Apr 25, 2026
e5ccfe3
Isolate package service auto-start failures
cursoragent Apr 25, 2026
aa62f27
Polish package service runtime lifecycle
cursoragent Apr 25, 2026
2cf25f4
Preserve null service timeout status
cursoragent Apr 25, 2026
7d0a93e
Reuse saved package in service listing
cursoragent Apr 25, 2026
e4e06f4
Drop unused package service auto-start helper
cursoragent Apr 25, 2026
2489d44
Rearm auto-start services after unexpected exits
cursoragent Apr 25, 2026
9217317
Share package service status normalization
cursoragent Apr 25, 2026
17cf643
Treat service_start as restart intent
cursoragent Apr 25, 2026
4678d67
Retry auto-start services after alarm load errors
cursoragent Apr 25, 2026
93d5de5
Back off auto-start alarm retries
cursoragent Apr 25, 2026
cf53f5b
Keep service status validation in runtime
cursoragent Apr 25, 2026
ad49d08
Honor autoStart when alarm retries fire
cursoragent Apr 25, 2026
cfe34b4
Back off runtime auto-start restarts
cursoragent Apr 25, 2026
31d3271
Polish package service review follow-ups
cursoragent Apr 25, 2026
7b06950
Trim package service runtime surface
cursoragent Apr 25, 2026
597effc
Harden package service registry calls
cursoragent Apr 25, 2026
07aba12
Align package app service list shape
cursoragent Apr 25, 2026
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
14 changes: 14 additions & 0 deletions docs/contributing/architecture/request-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ Requests are handled in this order:
9. App server routes:
- Everything else is handled by `packages/worker/src/app/handler.ts`

## Package service runtime

Saved packages may also declare long-lived package services under
`package.json#kody.services`.

- Package services are **not** routed through a public HTTP path the way package
apps are.
- Instead, the Worker hosts them via the `PackageServiceInstance` Durable Object
binding and controls them through package runtime bridges and MCP
capabilities.
- Package services share package identity with apps/jobs and can publish into
package app realtime sessions, but they own their own durable storage bucket
and lifecycle.

## App server flow

`packages/worker/src/app/handler.ts` validates environment variables and
Expand Down
7 changes: 7 additions & 0 deletions docs/use/execute.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ and export a default function. These helpers are runtime exports:
active execute or job params instead of receiving them as a function argument
- use **`import { packageContext } from 'kody:runtime'`** inside saved package
code when you need package metadata; it is **`null`** for ad hoc execute calls
- use **`import { serviceContext } from 'kody:runtime'`** inside package service
code when you need the current service identity; it is **`null`** outside
package service runs
- use **`import thing from 'kody:@scope/my-package/export-name'`** to reuse a
saved package export by full package name

Expand Down Expand Up @@ -52,6 +55,8 @@ module-oriented runtime model:
- package-specific metadata lives under `package.json#kody`
- package jobs are schedules declared under `package.json#kody.jobs`
- package apps are optional UI surfaces declared under `package.json#kody.app`
- package services are optional long-lived runtimes declared under
`package.json#kody.services`
- non-package jobs can also be scheduled directly with
**`codemode.job_schedule(...)`** without creating a saved package
- **`codemode.job_schedule_once(...)`** remains available as a convenience alias
Expand Down Expand Up @@ -94,6 +99,8 @@ package-owned jobs and non-package jobs created with `job_schedule` or
`job_schedule_once`.

- bound storage is execute-, app-, package-, or job-owned durable state
- package service runs also get writable service-owned durable state scoped to
the declared service name
- import **`storage`** from **`kody:runtime`**
- use **`storage.get(...)`**, **`storage.set(...)`**, **`storage.list(...)`**,
and **`storage.sql(query, params?)`**
Expand Down
24 changes: 24 additions & 0 deletions docs/use/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Think in terms of:
- packages
- package exports
- package apps
- package services
- package-owned jobs

Packages are the saved-entity unit across search, execute, repo editing, and UI
Expand All @@ -30,6 +31,7 @@ Important fields:
- `kody.description` — package description for search/detail
- `kody.tags` — package tags
- `kody.app` — optional hosted package app config
- `kody.services` — optional package-owned service runtimes
- `kody.jobs` — optional package-owned schedules

`package.json` is the manifest.
Expand Down Expand Up @@ -70,6 +72,28 @@ Treat package apps like Worker-style modules:
- internal Durable Objects or facets are implementation details, not the public
authoring contract

## Package services

A package service is optional.

When `package.json#kody.services` is present, the package can declare one or
more named service entrypoints that Kody runs with package-owned storage and
package caller context.

Use the package service model when the package needs:

- long-lived or repeated background work
- package-owned daemon-like logic
- package state that is separate from browser sessions
- a service that should publish updates into a package app

Treat package services like package-owned runtime modules:

- service code lives in the package repo
- each service entry module is declared by `kody.services.<name>.entry`
- service lifecycle is controlled through the `services` capability domain
- services share the same saved package identity as package apps and jobs

## Package-owned jobs

Packages can own jobs, and Kody also supports schedules that are not owned
Expand Down
12 changes: 12 additions & 0 deletions packages/worker/src/env-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,18 @@ export const EnvSchema = object({
context.path,
)
}),
PACKAGE_SERVICE_INSTANCE: createSchema<
unknown,
DurableObjectNamespace
>((value, context) => {
if (value) {
return { value: value as DurableObjectNamespace }
}
return fail(
'Missing PACKAGE_SERVICE_INSTANCE binding for package service runtimes.',
context.path,
)
}),
APP_BASE_URL: optionalUrlStringSchema,
APP_COMMIT_SHA: optionalCommitShaSchema,
CLOUDFLARE_EMAIL_FROM: optionalNonEmptyStringSchema,
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { StorageRunner } from './storage-runner.ts'
import { AgentTurnRunner } from './agent-turn/runner-do.ts'
import { RepoSession } from './repo/repo-session-do.ts'
import { PackageRealtimeSession } from '#worker/package-runtime/realtime-session.ts'
import { PackageServiceInstance } from '#worker/package-runtime/package-service.ts'
import { chatAgentBasePath } from '@kody-internal/shared/chat-routes.ts'
import { getWorkerSentryOptions } from './sentry-options.ts'
import { handleRequest } from '#app/handler.ts'
Expand Down Expand Up @@ -54,6 +55,7 @@ export {
MCP,
JobManager,
PackageRealtimeSession,
PackageServiceInstance,
PackageAppRuntimeBridge,
StorageRunner,
}
Expand Down
1 change: 1 addition & 0 deletions packages/worker/src/jobs/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ async function persistPublishedJobBundleArtifact(input: {
modules: bundle.modules,
dependencies: bundle.dependencies,
packageContext: input.packageContext ?? null,
serviceContext: null,
createdAt: new Date().toISOString(),
}
await persistPublishedBundleArtifact({
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/src/mcp/capabilities/builtin-domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { metaDomain } from './meta/domain.ts'
import { packagesDomain } from './packages/domain.ts'
import { repoDomain } from './repo/domain.ts'
import { secretsDomain } from './secrets/domain.ts'
import { servicesDomain } from './services/domain.ts'
import { storageDomain } from './storage/domain.ts'
import { valuesDomain } from './values/domain.ts'

Expand All @@ -24,6 +25,7 @@ export const builtinDomains = [
packagesDomain,
repoDomain,
secretsDomain,
servicesDomain,
storageDomain,
valuesDomain,
] as const
1 change: 1 addition & 0 deletions packages/worker/src/mcp/capabilities/domain-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const capabilityDomainNames = {
packages: 'packages',
repo: 'repo',
secrets: 'secrets',
services: 'services',
storage: 'storage',
values: 'values',
} as const
Expand Down
19 changes: 19 additions & 0 deletions packages/worker/src/mcp/capabilities/services/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineDomain } from '../define-domain.ts'
import { capabilityDomainNames } from '../domain-metadata.ts'
import { serviceGetCapability } from './service-get.ts'
import { serviceListCapability } from './service-list.ts'
import { serviceStartCapability } from './service-start.ts'
import { serviceStopCapability } from './service-stop.ts'

export const servicesDomain = defineDomain({
name: capabilityDomainNames.services,
description:
'Package service capabilities for inspecting and controlling long-lived package-owned runtimes.',
keywords: ['service', 'package service', 'runtime', 'background', 'gateway'],
capabilities: [
serviceListCapability,
serviceGetCapability,
serviceStartCapability,
serviceStopCapability,
],
})
42 changes: 42 additions & 0 deletions packages/worker/src/mcp/capabilities/services/service-get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
import { z } from 'zod'
import {
normalizePackageServiceStatus,
packageServiceStatusSchema,
requirePackageServiceContext,
} from './shared.ts'

export const serviceGetCapability = defineDomainCapability(
capabilityDomainNames.services,
{
name: 'service_get',
description:
'Get status for one package service declared under package.json#kody.services.',
keywords: ['service', 'status', 'package', 'inspect'],
readOnly: true,
idempotent: true,
destructive: false,
inputSchema: z.object({
service_name: z.string().trim().min(1),
package_id: z.string().min(1).optional(),
}),
outputSchema: packageServiceStatusSchema,
async handler(args, ctx) {
const serviceContext = await requirePackageServiceContext({
env: ctx.env,
callerContext: ctx.callerContext,
serviceName: args.service_name,
explicitPackageId: args.package_id,
})
if (!serviceContext.service) {
throw new Error(
`Package service "${args.service_name}" was not found.`,
)
}
return normalizePackageServiceStatus(
await serviceContext.service.status(),
)
},
},
)
63 changes: 63 additions & 0 deletions packages/worker/src/mcp/capabilities/services/service-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { z } from 'zod'
import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
import {
listPackageServicesForContext,
packageServiceSummarySchema,
normalizePackageServiceStatus,
} from './shared.ts'

const inputSchema = z.object({
package_id: z.string().min(1).optional(),
})

const outputSchema = z.object({
package_id: z.string(),
kody_id: z.string(),
services: z.array(packageServiceSummarySchema),
})

export const serviceListCapability = defineDomainCapability(
capabilityDomainNames.services,
{
name: 'service_list',
description:
'List package services declared by a saved package, including their current runtime status.',
keywords: ['service', 'services', 'package', 'list', 'status'],
readOnly: true,
idempotent: true,
destructive: false,
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
inputSchema,
outputSchema,
async handler(args, ctx) {
const listed = await listPackageServicesForContext({
env: ctx.env,
callerContext: ctx.callerContext,
explicitPackageId: args.package_id,
})
const services = await Promise.all(
listed.services.map(async (service) => {
let status: z.infer<typeof packageServiceSummarySchema>['status'] = 'error'
try {
status = normalizePackageServiceStatus(
await listed.rpc(service.name).status(),
).status
} catch {
// Keep the rest of the service list usable if one status lookup fails.
}
return {
name: service.name,
entry: service.entry,
auto_start: service.auto_start,
status,
}
}),
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
return {
package_id: listed.savedPackage.id,
kody_id: listed.savedPackage.kodyId,
services,
}
},
},
)
46 changes: 46 additions & 0 deletions packages/worker/src/mcp/capabilities/services/service-start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { z } from 'zod'
import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
import { requirePackageServiceContext } from './shared.ts'

const inputSchema = z.object({
service_name: z.string().min(1),
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
package_id: z.string().min(1).optional(),
})

const outputSchema = z.object({
ok: z.boolean(),
result: z.unknown().optional(),
error: z.string().optional(),
started_at: z.string().optional(),
finished_at: z.string().optional(),
})

export const serviceStartCapability = defineDomainCapability(
capabilityDomainNames.services,
{
name: 'service_start',
description:
'Start a package service instance and return the latest execution result.',
keywords: ['service', 'start', 'package service', 'runtime'],
readOnly: false,
idempotent: false,
destructive: false,
inputSchema,
outputSchema,
async handler(args, ctx) {
const serviceContext = await requirePackageServiceContext({
env: ctx.env,
callerContext: ctx.callerContext,
serviceName: args.service_name,
explicitPackageId: args.package_id,
})
if (!serviceContext.service) {
throw new Error(
`Package service "${args.service_name}" was not found for this package.`,
)
}
return await serviceContext.service.start()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
},
},
)
44 changes: 44 additions & 0 deletions packages/worker/src/mcp/capabilities/services/service-stop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { z } from 'zod'
import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
import { requirePackageServiceContext } from './shared.ts'

const inputSchema = z.object({
service_name: z.string().trim().min(1),
package_id: z.string().min(1).optional(),
})

const outputSchema = z.object({
ok: z.boolean(),
})

export const serviceStopCapability = defineDomainCapability(
capabilityDomainNames.services,
{
name: 'service_stop',
description: 'Stop one declared package service instance.',
keywords: ['service', 'stop', 'package'],
readOnly: false,
idempotent: false,
destructive: false,
inputSchema,
outputSchema,
async handler(args, ctx) {
const serviceContext = await requirePackageServiceContext({
env: ctx.env,
callerContext: ctx.callerContext,
serviceName: args.service_name,
explicitPackageId: args.package_id,
})
if (!serviceContext.service) {
throw new Error(
`Package service "${args.service_name}" was not found.`,
)
}
const result = (await serviceContext.service.stop()) as { ok?: unknown }
return {
ok: result?.ok === true,
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
},
},
)
Loading
Loading