tardie 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/package.json +2 -2
  2. package/src/agent/components/budget-authority.ts +2 -2
  3. package/src/agent/components/budget.ts +7 -7
  4. package/src/agent/components/compaction.ts +1 -1
  5. package/src/agent/components/permission-authority.ts +2 -2
  6. package/src/agent/index.ts +2 -2
  7. package/src/agent/log/events.ts +1 -1
  8. package/src/agent/packages/agents.ts +66 -42
  9. package/src/agent/runtime/resume.ts +6 -6
  10. package/src/bun/host.ts +357 -327
  11. package/src/channels/providers/slack.ts +2 -2
  12. package/src/channels/providers/telegram.ts +2 -2
  13. package/src/cli/celld.ts +4 -1
  14. package/src/cli/commands.ts +16 -10
  15. package/src/cli/config.ts +3 -3
  16. package/src/cli/init.ts +8 -5
  17. package/src/client/client.ts +42 -30
  18. package/src/client/contract.ts +49 -18
  19. package/src/client/index.ts +2 -0
  20. package/src/client/stream.ts +4 -2
  21. package/src/cloudflare/host.ts +71 -126
  22. package/src/cloudflare/index.ts +17 -1
  23. package/src/cloudflare/storage.ts +165 -51
  24. package/src/cloudflare/worker.ts +660 -188
  25. package/src/code/execution/errors.ts +1 -1
  26. package/src/code/execution/events.ts +3 -3
  27. package/src/code/execution/ids.ts +2 -2
  28. package/src/code/execution/projections.ts +6 -6
  29. package/src/code/execution/reactor.ts +4 -4
  30. package/src/code/package/workspace.ts +2 -2
  31. package/src/code/sandbox/defaults.ts +2 -2
  32. package/src/code/storage/store.ts +1 -1
  33. package/src/core/actor/method/outgoing.ts +3 -3
  34. package/src/core/actor/method/response.ts +12 -12
  35. package/src/core/actor/reference.ts +4 -3
  36. package/src/core/communication/endpoint.ts +44 -23
  37. package/src/core/communication/envelope.ts +6 -6
  38. package/src/core/log/service.ts +25 -0
  39. package/src/core/reconciliation/reconciler.ts +3 -3
  40. package/src/core/thread/lineage.ts +80 -16
  41. package/src/host/communication/channel.ts +2 -2
  42. package/src/host/communication/ingress.ts +2 -2
  43. package/src/host/deadlock.ts +22 -22
  44. package/src/host/driver.ts +40 -40
  45. package/src/host/host.ts +74 -100
  46. package/src/server/actor.ts +2 -2
  47. package/src/server/api.ts +55 -29
  48. package/src/server/config.ts +12 -12
  49. package/src/server/driver-gauge.ts +1 -1
  50. package/src/server/host.ts +186 -67
  51. package/src/server/http.ts +3 -1
  52. package/src/server/projections.ts +17 -12
  53. package/ui/assets/{index-H4ExQl4h.js → index-BN-2ETY9.js} +3 -3
  54. package/ui/index.html +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tardie",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "license": "MIT",
5
5
  "author": "Clavia, Inc.",
6
6
  "description": "A durable agent harness. State is a pure function of the log.",
@@ -16,7 +16,7 @@
16
16
  "access": "public"
17
17
  },
18
18
  "tardigrade": {
19
- "sourceTree": "97448ddae8564fb78c339ce7cc9423dfe7e18b73"
19
+ "sourceTree": "9d578e4ded2cd6f98b5b20d18d9c68df4a7622fa"
20
20
  },
21
21
  "files": [
22
22
  "src",
@@ -3,7 +3,7 @@ import { Schema } from "effect"
3
3
  import type { KeyFragment } from "tardie/core/log"
4
4
  import { intent, type Transition } from "tardie/core/reconciliation"
5
5
  import { handles, type Component } from "tardie/core/actor"
6
- import { formatActorId, isActorId } from "tardie/core/communication/endpoint"
6
+ import { formatThreadAddress, isThreadAddress } from "tardie/core/communication/endpoint"
7
7
  import { BudgetDecision } from "../actor/budget"
8
8
  import { budgetRequestDecided, budgetRequestFailed } from "../log/events"
9
9
  import { requestBudgetMethod } from "../actor/budget"
@@ -70,7 +70,7 @@ const authorityTransition = (
70
70
  turn: String(received.turn ?? ""),
71
71
  reason: String(received.reason ?? ""),
72
72
  amount,
73
- ...(isActorId(received.link?.source) ? { from: formatActorId(received.link.source) } : {}),
73
+ ...(isThreadAddress(received.link?.source) ? { from: formatThreadAddress(received.link.source) } : {}),
74
74
  grant: (granted = amount) => ({ granted }),
75
75
  deny: (reason) => ({ denied: true, ...(reason === undefined ? {} : { reason }) })
76
76
  }
@@ -13,7 +13,7 @@ import { turnHead, turnView } from "tardie/code/execution/turns"
13
13
  import { AGENT_VIEW_ALGEBRA, type AgentComponent, type AgentTool, type AgentView } from "../runtime/composition"
14
14
  import type { ToolSpec } from "../inference/request"
15
15
  import { Router } from "tardie/core/communication/router"
16
- import { formatActorId, isActorId, type ActorId } from "tardie/core/communication/endpoint"
16
+ import { formatThreadAddress, isThreadAddress, type ThreadAddress } from "tardie/core/communication/endpoint"
17
17
  import type { Link } from "tardie/core/communication/link"
18
18
  import { threadCreatedOf } from "tardie/core/thread"
19
19
  import { requestBudgetMethod } from "../actor/budget"
@@ -194,8 +194,8 @@ const requestBudgetTool: AgentTool = {
194
194
  }
195
195
  }
196
196
 
197
- const requestCallId = (child: ActorId, turn: string, request: string): string =>
198
- `budget/${formatActorId(child)}/${turn}/${request}`
197
+ const requestCallId = (child: ThreadAddress, turn: string, request: string): string =>
198
+ `budget/${formatThreadAddress(child)}/${turn}/${request}`
199
199
 
200
200
  const authorityFor = (
201
201
  log: ReadonlyArray<Event>,
@@ -206,17 +206,17 @@ const authorityFor = (
206
206
  if ("address" in authority) return authority
207
207
  const head = log.find((event) =>
208
208
  event.type === "MessageReceived" && String((event as { readonly id?: unknown }).id) === turn
209
- ) as { readonly link?: Link<unknown, ActorId> } | undefined
210
- return isActorId(head?.link?.source)
209
+ ) as { readonly link?: Link<unknown, ThreadAddress> } | undefined
210
+ return isThreadAddress(head?.link?.source)
211
211
  ? { address: head.link.source, methods: { requestBudget: requestBudgetMethod } }
212
212
  : undefined
213
213
  }
214
214
 
215
- const sourceFor = (log: ReadonlyArray<Event>, turn: string): ActorId | undefined => {
215
+ const sourceFor = (log: ReadonlyArray<Event>, turn: string): ThreadAddress | undefined => {
216
216
  const head = log.find((event) =>
217
217
  event.type === "MessageReceived" && String((event as { readonly id?: unknown }).id) === turn
218
218
  ) as { readonly link?: Link<unknown, unknown> } | undefined
219
- if (isActorId(head?.link?.target)) return head.link.target
219
+ if (isThreadAddress(head?.link?.target)) return head.link.target
220
220
  return threadCreatedOf(log)?.address
221
221
  }
222
222
 
@@ -167,7 +167,7 @@ export const resolvedContextPolicyOf = (policy: Partial<ContextPolicy> = {}): Co
167
167
 
168
168
  // renderedChars counts the characters a render sends for one event: capped where the render
169
169
  // caps, zero for an event the render skips. The guard must measure the request the model sees;
170
- // a measure over raw event JSON counts tool results the render truncates and lanes the render
170
+ // a measure over raw event JSON counts tool results the render truncates and threads the render
171
171
  // never shows, and fires against a size no request ever reaches.
172
172
  const renderedChars = (e: Event, policy: ContextPolicy): number => {
173
173
  const v = e as Record<string, unknown>
@@ -3,7 +3,7 @@ import { Schema } from "effect"
3
3
  import type { KeyFragment } from "tardie/core/log"
4
4
  import { intent, type Transition } from "tardie/core/reconciliation"
5
5
  import { externallyHandled, handles, type Component } from "tardie/core/actor"
6
- import { formatActorId, isActorId } from "tardie/core/communication/endpoint"
6
+ import { formatThreadAddress, isThreadAddress } from "tardie/core/communication/endpoint"
7
7
  import { PermissionDecision, requestPermissionMethod } from "../actor/permission"
8
8
  import { permissionRequestDecided, permissionRequestFailed } from "../log/events"
9
9
 
@@ -71,7 +71,7 @@ const authorityTransition = (
71
71
  action: String(received.action ?? ""),
72
72
  ...(typeof received.resource === "string" ? { resource: received.resource } : {}),
73
73
  reason: String(received.reason ?? ""),
74
- ...(isActorId(received.link?.source) ? { from: formatActorId(received.link.source) } : {}),
74
+ ...(isThreadAddress(received.link?.source) ? { from: formatThreadAddress(received.link.source) } : {}),
75
75
  grant: () => ({ granted: true }),
76
76
  deny: (reason) => ({ denied: true, ...(reason === undefined ? {} : { reason }) })
77
77
  }
@@ -140,8 +140,8 @@ export {
140
140
  // boundary is a projection of the log rather than a value the driver returns (boundary.ts).
141
141
  export { boundaryOf, outputOf, type Boundary } from "./output/boundary"
142
142
 
143
- // The spawn package: a value with no lane in it, so the assembly that mounts it and the host
144
- // that binds Router and Self per lane cannot disagree about placement (packages/agents.ts).
143
+ // The spawn package: a value with no thread in it, so the assembly that mounts it and the host
144
+ // that binds Router and Self per thread cannot disagree about placement (packages/agents.ts).
145
145
  export {
146
146
  agentsPackage,
147
147
  INLINE_OUTPUT_NAME,
@@ -377,7 +377,7 @@ export type Action =
377
377
  }
378
378
  } & Served)
379
379
 
380
- // agentKeys is the agent lane's dedup fragment, owned beside its alphabet. tr names the tool call's recorded
380
+ // agentKeys is the agent thread's dedup fragment, owned beside its alphabet. tr names the tool call's recorded
381
381
  // pair; bdec names the budget request a local decision answers, so a grant and denial for one request
382
382
  // cannot both commit. A grant is summed into the ceiling, so a redelivery must also absorb. A
383
383
  // decision that carries no callId predates the stamp and lands unkeyed; the fold tolerates it.
@@ -1,4 +1,4 @@
1
- import { Clock, Effect } from "effect"
1
+ import { Clock, Effect, Schema } from "effect"
2
2
  import { Router } from "tardie/core/communication/router"
3
3
  import { Self } from "tardie/core/reconciliation"
4
4
  import { EventLog } from "tardie/core/log"
@@ -8,11 +8,19 @@ import { Park } from "tardie/code/execution/errors"
8
8
  import { boundaryId } from "tardie/core/communication/message"
9
9
  import { linkOf } from "tardie/core/communication/link"
10
10
  import { methodEnvelopeOf } from "tardie/core/communication/envelope"
11
- import { childLineageOf, threadCreatedOf } from "tardie/core/thread"
12
11
  import {
13
- actorIdOf,
14
- formatActorId,
15
- type ActorId
12
+ ChildCreated,
13
+ childCreated,
14
+ childLineageOf,
15
+ ChildPlacement,
16
+ threadCreatedOf,
17
+ type ThreadCreated,
18
+ type ThreadLineage
19
+ } from "tardie/core/thread"
20
+ import {
21
+ threadAddressOf,
22
+ formatThreadAddress,
23
+ type ThreadAddress
16
24
  } from "tardie/core/communication/endpoint"
17
25
  import { decodeOutput, outputFrom, type OutputContract } from "../output/contract"
18
26
  import { modelRefOf } from "../inference/reference"
@@ -40,14 +48,11 @@ import {
40
48
  // message id, so a replayed dispatch reaches the same child and is absorbed as a duplicate.
41
49
  //
42
50
  // The package is a value any consumer mounts: its host privileges are services, not constructor
43
- // arguments. `Router` sends, `Self` names the calling lane, and `EventLog` supplies its durable
51
+ // arguments. `Router` sends, `Self` names the calling thread, and `EventLog` supplies its durable
44
52
  // calls, responses, and creation record.
45
53
  //
46
- // place selects the child's actor address from the call id and parent address. The host resolves
47
- // that stable identity to current placement when it interprets the resulting link.
48
- //
49
54
  // A plain foreground run parks, the same mechanism `tasks.fire` (`src/packages/tasks.ts`) uses:
50
- // deliver the brief through a link from this lane, then await the reply row on this lane, host-side
55
+ // deliver the brief through a link from this thread, then await the reply row on this thread, host-side
51
56
  // `Park` when it has not landed yet (`src/code/execute.ts`'s proxy is what turns that into a promise
52
57
  // that never settles for the code body). It never holds a call open.
53
58
  //
@@ -246,19 +251,46 @@ const catalogQueryOf = (args: unknown): AgentCatalogQuery => {
246
251
  }
247
252
  }
248
253
 
249
- // sibling is the default placement: the child is a facet of the parent's own principal, named `ag.<callId>`. The address selects the target while ThreadCreated records its lineage (spawn.test.ts, "the default placement is the host's own sibling address"; tla/runtime/Thread.tla, CreationFirst).
250
- const sibling = (callId: string, self: ActorId): ActorId =>
251
- actorIdOf(self.actor, `ag.${callId}`)
254
+ // sibling is the default address: the child inherits the parent's actor instance and uses the name `ag.<callId>`. Thread placement remains a separate host decision (spawn.test.ts, "the default address is the host's own sibling"; tla/runtime/Thread.tla, CreationFirst).
255
+ const sibling = (callId: string, self: ThreadAddress): ThreadAddress =>
256
+ threadAddressOf(self.actor, self.instance, `ag.${callId}`)
257
+
258
+ const childClaimOf = (
259
+ placement: unknown,
260
+ events: ReadonlyArray<{ readonly type: string }>,
261
+ parent: ThreadCreated,
262
+ callId: string,
263
+ source: ThreadAddress
264
+ ) => {
265
+ if (placement !== undefined && !Schema.is(ChildPlacement)(placement)) {
266
+ return { error: "agents.run placement must be colocated or independent" }
267
+ }
268
+ const recorded = events.find(
269
+ (event) => event.type === "ChildCreated" && (event as { readonly callId?: unknown }).callId === callId
270
+ )
271
+ if (recorded !== undefined && !Schema.is(ChildCreated)(recorded)) {
272
+ throw new Error(`child ${callId} has an invalid creation record`)
273
+ }
274
+ const lineage: ThreadLineage = recorded === undefined
275
+ ? childLineageOf(parent, placement as ChildPlacement | undefined)
276
+ : {
277
+ parent: parent.address,
278
+ depth: recorded.depth,
279
+ ...(recorded.placement === undefined ? {} : { placement: recorded.placement })
280
+ }
281
+ return {
282
+ recorded,
283
+ target: recorded?.address ?? sibling(callId, source),
284
+ lineage
285
+ }
286
+ }
252
287
 
253
288
  const inheritedModelsOf = (events: ReadonlyArray<{ readonly type: string }>): ModelPolicy => {
254
289
  const resolved = [...events].reverse().find((event) => event.type === "ModelResolved") as { readonly models?: unknown } | undefined
255
290
  return resolved?.models === undefined ? DEFAULT_MODEL_POLICY : modelPolicyOf(resolved.models)
256
291
  }
257
292
 
258
- export const agentsPackage = (
259
- options: SpawnOptions & { readonly place?: (callId: string, self: ActorId) => ActorId } = {}
260
- ): Package<Router | Self | EventLog> => {
261
- const place = options.place ?? sibling
293
+ export const agentsPackage = (options: SpawnOptions = {}): Package<Router | Self | EventLog> => {
262
294
  const actorNameOf = options.actorNameOf ?? (() => undefined)
263
295
  const reserve = options.reserve ?? (async (_callId: string, want: number) => want)
264
296
  const shadowOf = options.shadowOf ?? (() => false)
@@ -331,6 +363,7 @@ export const agentsPackage = (
331
363
  additionalProperties: false
332
364
  },
333
365
  budget: { type: "integer", description: "max tool calls before the agent must answer, a whole number of calls; keeps a research agent bounded" },
366
+ placement: { type: "string", enum: ["colocated", "independent"], description: "place the child relative to this thread's host" },
334
367
  escalatable: { type: "boolean", description: "true: at its budget the child may ask its parent's budget authority for more before answering" }
335
368
  },
336
369
  required: ["text"]
@@ -394,7 +427,7 @@ export const agentsPackage = (
394
427
  }),
395
428
  run: (args, ctx) =>
396
429
  Effect.gen(function* () {
397
- // The three cross-lane privileges, read where the work happens: send, identity,
430
+ // The three cross-thread privileges, read where the work happens: send, identity,
398
431
  // observe. A host that binds them serves this method; nothing here closes over one.
399
432
  const router = yield* Router
400
433
  const source = yield* Self
@@ -402,15 +435,17 @@ export const agentsPackage = (
402
435
  const events = yield* log.read
403
436
  const created = threadCreatedOf(events)
404
437
  if (created === undefined) {
405
- return yield* Effect.die(new Error(`thread ${formatActorId(source)} cannot spawn without ThreadCreated`))
438
+ return yield* Effect.die(new Error(`thread ${formatThreadAddress(source)} cannot spawn without ThreadCreated`))
406
439
  }
407
- const lineage = childLineageOf(created)
408
- const self = formatActorId(source)
440
+ const self = formatThreadAddress(source)
409
441
  const a = args as
410
- | { text?: unknown; background?: unknown; output?: unknown; outputSchema?: unknown; model?: unknown; budget?: unknown; escalatable?: unknown }
442
+ | { text?: unknown; background?: unknown; output?: unknown; outputSchema?: unknown; model?: unknown; budget?: unknown; escalatable?: unknown; placement?: unknown }
411
443
  | undefined
412
444
  const text = String(a?.text ?? "")
413
445
  if (text === "") return { error: "agents.run needs { text }" }
446
+ const child = childClaimOf(a?.placement, events, created, ctx.callId, source)
447
+ if ("error" in child) return child
448
+ const { lineage, recorded: recordedChild, target } = child
414
449
  // The contract parameter is `output`, and a near-miss spelling fails silently: no
415
450
  // contract means a prose answer, so the caller's field reads come back undefined and
416
451
  // the run returns something plausible and wrong. Say so instead.
@@ -454,11 +489,8 @@ export const agentsPackage = (
454
489
  // the same way, when the fire named an explicit shared one.
455
490
  const shadow = shadowOf()
456
491
  const world = worldOf()
457
- const target = place(ctx.callId, source)
458
- if (a?.background === true) {
459
- // A background run uses the same actor protocol. Its budget request can be served while
460
- // no code body awaits it, and its terminal is collected later by agents.result.
461
- const at = yield* Clock.currentTimeMillis
492
+ const dispatch = (at: number) => Effect.gen(function* () {
493
+ if (recordedChild === undefined) yield* log.append([childCreated(ctx.callId, target, lineage, at)])
462
494
  yield* router.send(methodEnvelopeOf(linkOf(source, target), { method: "message", id: ctx.callId }, {
463
495
  type: "MessageReceived",
464
496
  id: ctx.callId,
@@ -474,6 +506,12 @@ export const agentsPackage = (
474
506
  from: self,
475
507
  at
476
508
  }, lineage))
509
+ })
510
+ if (a?.background === true) {
511
+ // A background run uses the same actor protocol. Its budget request can be served while
512
+ // no code body awaits it, and its terminal is collected later by agents.result.
513
+ const at = yield* Clock.currentTimeMillis
514
+ yield* dispatch(at)
477
515
  return { dispatched: true, callId: ctx.callId }
478
516
  }
479
517
  // Foreground runs park on their terminal response. A replay reads that response
@@ -481,21 +519,7 @@ export const agentsPackage = (
481
519
  const already = yield* awaitedBoundary(ctx.callId)
482
520
  if (already !== undefined) return shape(answerOf(already), ctx.callId, output)
483
521
  const at = yield* Clock.currentTimeMillis
484
- yield* router.send(methodEnvelopeOf(linkOf(source, target), { method: "message", id: ctx.callId }, {
485
- type: "MessageReceived",
486
- id: ctx.callId,
487
- text,
488
- ...(outputDeclaration === undefined ? {} : { output: outputDeclaration }),
489
- ...(selectedModel === undefined ? {} : { model: selectedModel }),
490
- models,
491
- budget,
492
- ...(a?.escalatable === true ? { escalatable: true } : {}),
493
- ...(actor === undefined ? {} : { actor }),
494
- ...(shadow ? { shadow: true } : {}),
495
- ...(world === undefined ? {} : { world }),
496
- from: self,
497
- at
498
- }, lineage))
522
+ yield* dispatch(at)
499
523
  return yield* new Park({ callId: ctx.callId, awaiting: boundaryId(ctx.callId, 0) })
500
524
  }),
501
525
  // Await a run already fired in the background: no delivery, the same reply-or-park read the
@@ -7,10 +7,10 @@ type Awaitable<T> = T | Promise<T>
7
7
 
8
8
  // TurnDriver is the common control surface of the memory host and the durable Bun host.
9
9
  export interface TurnDriver {
10
- readonly read: (lane: string) => Awaitable<ReadonlyArray<Event>>
10
+ readonly read: (thread: string) => Awaitable<ReadonlyArray<Event>>
11
11
  readonly commitRoot: (address: string, event: Event) => Awaitable<void>
12
12
  readonly drive: () => Promise<void>
13
- readonly self: (lane: string) => string
13
+ readonly self: (thread: string) => string
14
14
  }
15
15
 
16
16
  export interface ResumeTurnOptions {
@@ -20,20 +20,20 @@ export interface ResumeTurnOptions {
20
20
  // resumeTurn starts a new execution epoch from a failed turn's committed history.
21
21
  export const resumeTurn = async (
22
22
  host: TurnDriver,
23
- lane: string,
23
+ thread: string,
24
24
  turn: string,
25
25
  options: ResumeTurnOptions = {}
26
26
  ): Promise<Boundary | undefined> => {
27
- const before = await host.read(lane)
27
+ const before = await host.read(thread)
28
28
  const boundary = boundaryOf(before, turn)
29
29
  if (boundary?.kind !== "failed") {
30
30
  throw new Error(`turn "${turn}" cannot resume because its active epoch is not failed`)
31
31
  }
32
32
  const failedEpoch = turnEpochOf(before, turn)
33
33
  await host.commitRoot(
34
- host.self(lane),
34
+ host.self(thread),
35
35
  turnResumed({ turn, failedEpoch, epoch: failedEpoch + 1, at: options.at ?? Date.now() })
36
36
  )
37
37
  await host.drive()
38
- return boundaryOf(await host.read(lane), turn)
38
+ return boundaryOf(await host.read(thread), turn)
39
39
  }