@a-dray/aglib 0.1.0 → 0.2.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 (42) hide show
  1. package/README.md +55 -136
  2. package/dist/agent.d.ts +12 -0
  3. package/dist/agent.js.map +1 -1
  4. package/dist/harness/adapters/acp/index.d.ts +27 -11
  5. package/dist/harness/adapters/acp/index.js +58 -24
  6. package/dist/harness/adapters/acp/index.js.map +1 -1
  7. package/dist/harness/adapters/native/compaction.d.ts +15 -0
  8. package/dist/harness/adapters/native/compaction.js +23 -8
  9. package/dist/harness/adapters/native/compaction.js.map +1 -1
  10. package/dist/harness/adapters/native/loop.d.ts +18 -1
  11. package/dist/harness/adapters/native/loop.js +0 -1
  12. package/dist/harness/adapters/native/loop.js.map +1 -1
  13. package/dist/harness/harness.d.ts +17 -6
  14. package/dist/harness/harness.js.map +1 -1
  15. package/dist/model/adapters/anthropic/index.js +21 -4
  16. package/dist/model/adapters/anthropic/index.js.map +1 -1
  17. package/dist/model/index.d.ts +0 -1
  18. package/dist/model/index.js +1 -1
  19. package/dist/model/index.js.map +1 -1
  20. package/dist/model/model.d.ts +8 -1
  21. package/dist/model/model.js +8 -1
  22. package/dist/model/model.js.map +1 -1
  23. package/dist/render.d.ts +100 -0
  24. package/dist/render.js +370 -0
  25. package/dist/render.js.map +1 -0
  26. package/dist/run.js +1 -0
  27. package/dist/run.js.map +1 -1
  28. package/dist/session/entry.d.ts +3 -2
  29. package/dist/session/entry.js.map +1 -1
  30. package/dist/session/messages.d.ts +9 -0
  31. package/dist/session/messages.js +5 -5
  32. package/dist/session/messages.js.map +1 -1
  33. package/dist/store/adapters/sqlite.js +25 -2
  34. package/dist/store/adapters/sqlite.js.map +1 -1
  35. package/dist/terminal.d.ts +23 -0
  36. package/dist/terminal.js +90 -0
  37. package/dist/terminal.js.map +1 -0
  38. package/dist/tools/execute.js +3 -1
  39. package/dist/tools/execute.js.map +1 -1
  40. package/dist/tools/tool.d.ts +8 -1
  41. package/dist/tools/tool.js.map +1 -1
  42. package/package.json +10 -1
@@ -152,9 +152,10 @@ export interface Delivery {
152
152
  input: Content;
153
153
  /**
154
154
  * Who sent it. Absent means the application did not say — not that nobody
155
- * did. Recorded on the receiving `run.started`, so a recipient can tell a
155
+ * did. Recorded on the receiving `run.started`, so an application can tell a
156
156
  * peer's message from its user's, and can see that it already answered one
157
- * that arrives twice.
157
+ * that arrives twice. It becomes text the model reads only where the
158
+ * recipient's agent asks, with `attribution`.
158
159
  */
159
160
  from?: From;
160
161
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../src/session/entry.ts"],"names":[],"mappings":"","sourcesContent":["import type { Content } from \"../content.js\";\nimport type { Failure } from \"../result.js\";\nimport type { JsonValue } from \"../json.js\";\n\n/** A model's request to invoke a tool. `arguments` is unparsed JSON as the provider sent it. */\nexport interface ToolCall {\n callId: string;\n name: string;\n arguments: string;\n}\n\nexport interface ToolResult {\n content: Content;\n /** Structured channel for the application and the UI. Never reaches the model. */\n details?: JsonValue;\n isError?: boolean;\n}\n\n/**\n * What a generation consumed.\n *\n * Absent stays absent — a count nobody reported is not a zero. This package\n * holds no rates and computes no money from tokens. Where an adapter does\n * arithmetic it is reconciling its wire's encoding to the meanings below:\n * normalization, which is an adapter's job, and never a new fact.\n *\n * That is why `costUsd` belongs here and a rate table does not. A cost the\n * provider *states* is an observation like the counts beside it, and dropping\n * it was the same mistake as dropping the model that served the request: an\n * application was left reconstructing, from a table it maintains by hand, a\n * number the wire had already given it. For a router that is not even possible\n * — it picks an upstream provider per request and adds its own margin, so no\n * static table can say what was charged.\n */\nexport interface Usage {\n /**\n * Input tokens that were neither read from nor written to a cache.\n *\n * **The three input counts are disjoint.** The prompt's total size is\n * `inputTokens + cacheReadTokens + cacheWriteTokens`; no field here is a\n * subset of another, so a caller sums and never subtracts.\n *\n * The wires disagree about this and one of them has to be converted. What\n * Anthropic calls `input_tokens` is already this — \"tokens which were not\n * read from or used to create a cache\". What OpenAI calls `prompt_tokens` is\n * the whole prompt, with `cached_tokens` counted inside it, so that adapter\n * subtracts once and callers stop having to know which wire answered.\n *\n * Disjoint is the choice that survives absence, which is the rule the rest of\n * this type runs on. Summing the fields you were given is right whichever\n * ones are missing; subtracting a field you were not given is not. Under the\n * other convention a heavily cached Anthropic turn priced its fresh tokens at\n * zero — quietly, because the subtraction was clamped at zero rather than\n * being allowed to go negative where somebody would have seen it.\n */\n inputTokens?: number;\n outputTokens?: number;\n /** Input tokens served from a cache. Disjoint from `inputTokens`. */\n cacheReadTokens?: number;\n /** Input tokens written to a cache. Disjoint from `inputTokens`. */\n cacheWriteTokens?: number;\n /**\n * What the provider said this generation cost, in US dollars. Reported,\n * never derived: no adapter computes this from a rate, and one whose wire\n * does not carry a cost leaves it absent rather than estimating.\n */\n costUsd?: number;\n}\n\n/**\n * Who sent an arrival: what sort of sender, and which one.\n *\n * aglib mints exactly one kind — `\"session\"`, a delivery from another session —\n * and interprets no other. Every other kind is the application's word for one of\n * its own senders: a person, a channel, a schedule, a webhook. The vocabulary\n * belongs to whoever can close it, and that is never this package.\n *\n * Two fields because absence was carrying four facts. This used to be the\n * sending session's id alone, so an operator, a channel and a timer all said the\n * same thing by saying nothing, and a recipient could not tell a person typing\n * from a routine firing. Packing both into one string instead — `\"routine:x\"` —\n * would put the vocabulary in a convention nothing closes, which is how such a\n * field ends up with values that exist only by grep and a different reading on\n * every surface that renders it.\n */\nexport interface From {\n kind: string;\n id: string;\n}\n\n/**\n * The canonical vocabulary. This is the session's state, not a record of it:\n * the loop reads its context by projecting these and appends back to them, so\n * there is no second transcript that could disagree.\n *\n * `tool.started` carries only the call id — the assistant entry above it\n * already holds the call, and duplicating name and arguments would be a second\n * field for a value another field determines.\n */\nexport type Entry =\n | { type: \"run.started\"; runId: string; input: Content; from?: From }\n | {\n type: \"assistant\"; runId: string; content: Content;\n calls?: readonly ToolCall[]; usage?: Usage;\n /**\n * What produced this turn, when a model did: which model answered and the\n * span it took. `Stored.at` is when the entry was committed, which is a\n * different fact and not a substitute — anything projecting a generation\n * needs both ends of the call, and the commit is neither of them.\n */\n generation?: { model?: string; startedAt: string; endedAt: string };\n }\n | { type: \"tool.started\"; runId: string; callId: string }\n | { type: \"tool.finished\"; runId: string; callId: string; result: ToolResult }\n /** Compaction output. `replaces` is the seq up to which entries are folded; nothing is deleted. */\n | { type: \"summary\"; runId: string; content: string; replaces: number }\n | { type: \"run.finished\"; runId: string; outcome: \"completed\" | \"failed\" | \"cancelled\"; error?: Failure };\n\nexport type EntryType = Entry[\"type\"];\n\n/** An entry once the log has given it a position. */\nexport type Stored<T extends Entry = Entry> = T & { seq: number; at: string };\n\n/**\n * Input delivered to a session, committed with the sender's own entries.\n * Spawning a child, replying to a parent and messaging a peer are all this.\n */\nexport interface Delivery {\n /** The session it is delivered to. */\n sessionId: string;\n input: Content;\n /**\n * Who sent it. Absent means the application did not say — not that nobody\n * did. Recorded on the receiving `run.started`, so a recipient can tell a\n * peer's message from its user's, and can see that it already answered one\n * that arrives twice.\n */\n from?: From;\n /**\n * Where in the recipient's loop this lands. Defaults to \"next\".\n *\n * Three places, and each is an outcome a sender can reason about rather than\n * a level of urgency a harness interprets:\n *\n * \"interrupt\" — the running activation ends, so the next one begins with\n * this. Its uncommitted work is lost; everything it had\n * already committed stays.\n * \"turn\" — folded into the activation already running, before its next\n * model call. Nothing in flight is lost. Immediately, if\n * nothing is running.\n * \"next\" — at the start of the recipient's next activation.\n *\n * This used to be `now | next | later`, and `now` meant either of the first\n * two depending on the harness — fold if it had a safe point, end the\n * activation if it did not. One word for \"your message arrives and the work\n * continues\" and \"your message arrives and a turn's work is destroyed\". The\n * doctrine was that a priority names the requirement and not the mechanism,\n * but those are not two mechanisms for one requirement: they are two\n * different things happening to somebody's work, and a runtime answer saying\n * which one you got was invented to paper over it. `later` named a fourth\n * thing that nothing implemented.\n *\n * **A harness that cannot honour one falls back to a later place, never an\n * earlier one.** A loop with no safe point cannot do \"turn\", so it does\n * \"next\" — the message waits. It does not end the activation instead, which\n * is what falling back through an ordered scale used to do: ask for the\n * gentlest useful thing and get the most destructive one.\n */\n priority?: \"interrupt\" | \"turn\" | \"next\";\n /**\n * The sender's key for this delivery. Two deliveries with the same id are the\n * same delivery, so a retried send does not arrive twice.\n */\n id?: string;\n}\n"]}
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../src/session/entry.ts"],"names":[],"mappings":"","sourcesContent":["import type { Content } from \"../content.js\";\nimport type { Failure } from \"../result.js\";\nimport type { JsonValue } from \"../json.js\";\n\n/** A model's request to invoke a tool. `arguments` is unparsed JSON as the provider sent it. */\nexport interface ToolCall {\n callId: string;\n name: string;\n arguments: string;\n}\n\nexport interface ToolResult {\n content: Content;\n /** Structured channel for the application and the UI. Never reaches the model. */\n details?: JsonValue;\n isError?: boolean;\n}\n\n/**\n * What a generation consumed.\n *\n * Absent stays absent — a count nobody reported is not a zero. This package\n * holds no rates and computes no money from tokens. Where an adapter does\n * arithmetic it is reconciling its wire's encoding to the meanings below:\n * normalization, which is an adapter's job, and never a new fact.\n *\n * That is why `costUsd` belongs here and a rate table does not. A cost the\n * provider *states* is an observation like the counts beside it, and dropping\n * it was the same mistake as dropping the model that served the request: an\n * application was left reconstructing, from a table it maintains by hand, a\n * number the wire had already given it. For a router that is not even possible\n * — it picks an upstream provider per request and adds its own margin, so no\n * static table can say what was charged.\n */\nexport interface Usage {\n /**\n * Input tokens that were neither read from nor written to a cache.\n *\n * **The three input counts are disjoint.** The prompt's total size is\n * `inputTokens + cacheReadTokens + cacheWriteTokens`; no field here is a\n * subset of another, so a caller sums and never subtracts.\n *\n * The wires disagree about this and one of them has to be converted. What\n * Anthropic calls `input_tokens` is already this — \"tokens which were not\n * read from or used to create a cache\". What OpenAI calls `prompt_tokens` is\n * the whole prompt, with `cached_tokens` counted inside it, so that adapter\n * subtracts once and callers stop having to know which wire answered.\n *\n * Disjoint is the choice that survives absence, which is the rule the rest of\n * this type runs on. Summing the fields you were given is right whichever\n * ones are missing; subtracting a field you were not given is not. Under the\n * other convention a heavily cached Anthropic turn priced its fresh tokens at\n * zero — quietly, because the subtraction was clamped at zero rather than\n * being allowed to go negative where somebody would have seen it.\n */\n inputTokens?: number;\n outputTokens?: number;\n /** Input tokens served from a cache. Disjoint from `inputTokens`. */\n cacheReadTokens?: number;\n /** Input tokens written to a cache. Disjoint from `inputTokens`. */\n cacheWriteTokens?: number;\n /**\n * What the provider said this generation cost, in US dollars. Reported,\n * never derived: no adapter computes this from a rate, and one whose wire\n * does not carry a cost leaves it absent rather than estimating.\n */\n costUsd?: number;\n}\n\n/**\n * Who sent an arrival: what sort of sender, and which one.\n *\n * aglib mints exactly one kind — `\"session\"`, a delivery from another session —\n * and interprets no other. Every other kind is the application's word for one of\n * its own senders: a person, a channel, a schedule, a webhook. The vocabulary\n * belongs to whoever can close it, and that is never this package.\n *\n * Two fields because absence was carrying four facts. This used to be the\n * sending session's id alone, so an operator, a channel and a timer all said the\n * same thing by saying nothing, and a recipient could not tell a person typing\n * from a routine firing. Packing both into one string instead — `\"routine:x\"` —\n * would put the vocabulary in a convention nothing closes, which is how such a\n * field ends up with values that exist only by grep and a different reading on\n * every surface that renders it.\n */\nexport interface From {\n kind: string;\n id: string;\n}\n\n/**\n * The canonical vocabulary. This is the session's state, not a record of it:\n * the loop reads its context by projecting these and appends back to them, so\n * there is no second transcript that could disagree.\n *\n * `tool.started` carries only the call id — the assistant entry above it\n * already holds the call, and duplicating name and arguments would be a second\n * field for a value another field determines.\n */\nexport type Entry =\n | { type: \"run.started\"; runId: string; input: Content; from?: From }\n | {\n type: \"assistant\"; runId: string; content: Content;\n calls?: readonly ToolCall[]; usage?: Usage;\n /**\n * What produced this turn, when a model did: which model answered and the\n * span it took. `Stored.at` is when the entry was committed, which is a\n * different fact and not a substitute — anything projecting a generation\n * needs both ends of the call, and the commit is neither of them.\n */\n generation?: { model?: string; startedAt: string; endedAt: string };\n }\n | { type: \"tool.started\"; runId: string; callId: string }\n | { type: \"tool.finished\"; runId: string; callId: string; result: ToolResult }\n /** Compaction output. `replaces` is the seq up to which entries are folded; nothing is deleted. */\n | { type: \"summary\"; runId: string; content: string; replaces: number }\n | { type: \"run.finished\"; runId: string; outcome: \"completed\" | \"failed\" | \"cancelled\"; error?: Failure };\n\nexport type EntryType = Entry[\"type\"];\n\n/** An entry once the log has given it a position. */\nexport type Stored<T extends Entry = Entry> = T & { seq: number; at: string };\n\n/**\n * Input delivered to a session, committed with the sender's own entries.\n * Spawning a child, replying to a parent and messaging a peer are all this.\n */\nexport interface Delivery {\n /** The session it is delivered to. */\n sessionId: string;\n input: Content;\n /**\n * Who sent it. Absent means the application did not say — not that nobody\n * did. Recorded on the receiving `run.started`, so an application can tell a\n * peer's message from its user's, and can see that it already answered one\n * that arrives twice. It becomes text the model reads only where the\n * recipient's agent asks, with `attribution`.\n */\n from?: From;\n /**\n * Where in the recipient's loop this lands. Defaults to \"next\".\n *\n * Three places, and each is an outcome a sender can reason about rather than\n * a level of urgency a harness interprets:\n *\n * \"interrupt\" — the running activation ends, so the next one begins with\n * this. Its uncommitted work is lost; everything it had\n * already committed stays.\n * \"turn\" — folded into the activation already running, before its next\n * model call. Nothing in flight is lost. Immediately, if\n * nothing is running.\n * \"next\" — at the start of the recipient's next activation.\n *\n * This used to be `now | next | later`, and `now` meant either of the first\n * two depending on the harness — fold if it had a safe point, end the\n * activation if it did not. One word for \"your message arrives and the work\n * continues\" and \"your message arrives and a turn's work is destroyed\". The\n * doctrine was that a priority names the requirement and not the mechanism,\n * but those are not two mechanisms for one requirement: they are two\n * different things happening to somebody's work, and a runtime answer saying\n * which one you got was invented to paper over it. `later` named a fourth\n * thing that nothing implemented.\n *\n * **A harness that cannot honour one falls back to a later place, never an\n * earlier one.** A loop with no safe point cannot do \"turn\", so it does\n * \"next\" — the message waits. It does not end the activation instead, which\n * is what falling back through an ordered scale used to do: ask for the\n * gentlest useful thing and get the most destructive one.\n */\n priority?: \"interrupt\" | \"turn\" | \"next\";\n /**\n * The sender's key for this delivery. Two deliveries with the same id are the\n * same delivery, so a retried send does not arrive twice.\n */\n id?: string;\n}\n"]}
@@ -39,4 +39,13 @@ export declare function toMessages(input: {
39
39
  run?: string;
40
40
  turn?: string;
41
41
  };
42
+ /**
43
+ * Name each arrival's sender in the turn text, as `[from kind id]`.
44
+ *
45
+ * Off by default. `from` is provenance the log keeps whatever this says; an
46
+ * application that renders its own attribution into the input it delivers
47
+ * would otherwise hand the model two names for one sender, one of them a
48
+ * session id that means nothing to it.
49
+ */
50
+ attribution?: boolean;
42
51
  }): readonly Message[];
@@ -9,7 +9,7 @@ export function foldedThrough(entries) {
9
9
  return entries.reduce((at, entry) => entry.type === "summary" ? Math.max(at, entry.replaces) : at, 0);
10
10
  }
11
11
  /** Names the sender in the turn itself, since only text reaches the model. */
12
- function sent(input, from) {
12
+ function labelled(input, from) {
13
13
  if (!from)
14
14
  return input;
15
15
  const label = `[from ${from.kind} ${from.id}]`;
@@ -49,10 +49,10 @@ export function toMessages(input) {
49
49
  continue;
50
50
  switch (entry.type) {
51
51
  case "run.started":
52
- // Provenance the model can see. `from` was recorded and then dropped
53
- // here, so the documents claimed a recipient could tell a peer's
54
- // message from its user's while the only actor that had to could not.
55
- messages.push({ role: "user", content: sent(entry.input, entry.from) });
52
+ messages.push({
53
+ role: "user",
54
+ content: input.attribution ? labelled(entry.input, entry.from) : entry.input,
55
+ });
56
56
  break;
57
57
  case "assistant": {
58
58
  messages.push({
@@ -1 +1 @@
1
- {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/session/messages.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Content } from \"../content.js\";\nimport type { From, Stored, ToolCall } from \"./entry.js\";\n\n/** A message as a provider takes it: built per turn from the log, never held. */\nexport type Message =\n | { role: \"system\"; content: Content }\n | { role: \"user\"; content: Content }\n | { role: \"assistant\"; content: Content; calls?: readonly ToolCall[] }\n | { role: \"tool\"; callId: string; content: Content; isError?: boolean };\n\n/**\n * The position everything up to has been folded into a summary.\n *\n * One answer, because two callers need it: the projection skips what is folded,\n * and the loop refuses to cut at or behind it. They were the same reduce written\n * out twice.\n */\nexport function foldedThrough(entries: readonly Stored[]): number {\n return entries.reduce((at, entry) => entry.type === \"summary\" ? Math.max(at, entry.replaces) : at, 0);\n}\n\n/** Names the sender in the turn itself, since only text reaches the model. */\nfunction sent(input: Content, from: From | undefined): Content {\n if (!from) return input;\n const label = `[from ${from.kind} ${from.id}]`;\n return typeof input === \"string\"\n ? `${label}\\n${input}`\n : [{ type: \"text\" as const, text: label }, ...input];\n}\n\n/**\n * The log projected into what a provider takes.\n *\n * Built fresh for every request and never held. That is the whole reason there\n * is no second transcript to drift from the log, and why nothing needs a test\n * proving two representations agree.\n */\nexport function toMessages(input: {\n instructions: Content;\n entries: readonly Stored[];\n context?: { run?: string; turn?: string };\n}): readonly Message[] {\n const messages: Message[] = [{ role: \"system\", content: input.instructions }];\n\n // Run-scoped context sits immediately after the instructions, inside the\n // cacheable prefix, because it does not change for the life of the run.\n if (input.context?.run) messages.push({ role: \"system\", content: input.context.run });\n\n // A summary folds everything up to `replaces`. The source entries stay in the\n // log — compaction changes what the model sees, never what happened.\n const cut = foldedThrough(input.entries);\n\n // A call whose result never committed. It happens when an activation ends\n // between asking and answering — cancelled, interrupted, or beaten to the\n // commit by another worker. The projection has to close it: a provider\n // rejects an assistant turn holding a call with no result, so leaving the gap\n // would make the session permanently unusable. What is said is what is known\n // — the call did not report back — and never an invented result.\n const answered = new Set(\n input.entries.filter((entry) => entry.type === \"tool.finished\").map((entry) => entry.callId),\n );\n\n for (const entry of input.entries) {\n // A summary folds like anything else it covers. Exempting the whole type\n // kept every summary ever written, so a long session carried a chain of\n // them whose content was already inside the newest — duplicated, and\n // rewriting the cached prefix each time one was added.\n if (entry.seq <= cut) continue;\n switch (entry.type) {\n case \"run.started\":\n // Provenance the model can see. `from` was recorded and then dropped\n // here, so the documents claimed a recipient could tell a peer's\n // message from its user's while the only actor that had to could not.\n messages.push({ role: \"user\", content: sent(entry.input, entry.from) });\n break;\n case \"assistant\": {\n messages.push({\n role: \"assistant\",\n content: entry.content,\n ...(entry.calls?.length ? { calls: entry.calls } : {}),\n });\n for (const call of entry.calls ?? []) {\n if (answered.has(call.callId)) continue;\n messages.push({\n role: \"tool\", callId: call.callId, isError: true,\n content: \"This call did not report back: the activation ended before its result was committed.\",\n });\n }\n break;\n }\n case \"tool.finished\":\n messages.push({\n role: \"tool\",\n callId: entry.callId,\n content: entry.result.content,\n ...(entry.result.isError ? { isError: true } : {}),\n });\n break;\n case \"summary\":\n // Delimited user context rather than an assistant turn: attributing a\n // summary to the assistant puts words in the model's mouth it never said.\n messages.push({ role: \"user\", content: `<summary>\\n${entry.content}\\n</summary>` });\n break;\n // Not model-visible: tool.started is bookkeeping and run.finished is a\n // boundary.\n case \"tool.started\":\n case \"run.finished\":\n break;\n }\n }\n\n // Turn-scoped context sits last, after the cache boundary, because it is for\n // this request only and must not be written into the cached prefix.\n if (input.context?.turn) messages.push({ role: \"system\", content: input.context.turn });\n return messages;\n}\n\n"]}
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/session/messages.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Content } from \"../content.js\";\nimport type { From, Stored, ToolCall } from \"./entry.js\";\n\n/** A message as a provider takes it: built per turn from the log, never held. */\nexport type Message =\n | { role: \"system\"; content: Content }\n | { role: \"user\"; content: Content }\n | { role: \"assistant\"; content: Content; calls?: readonly ToolCall[] }\n | { role: \"tool\"; callId: string; content: Content; isError?: boolean };\n\n/**\n * The position everything up to has been folded into a summary.\n *\n * One answer, because two callers need it: the projection skips what is folded,\n * and the loop refuses to cut at or behind it. They were the same reduce written\n * out twice.\n */\nexport function foldedThrough(entries: readonly Stored[]): number {\n return entries.reduce((at, entry) => entry.type === \"summary\" ? Math.max(at, entry.replaces) : at, 0);\n}\n\n/** Names the sender in the turn itself, since only text reaches the model. */\nfunction labelled(input: Content, from: From | undefined): Content {\n if (!from) return input;\n const label = `[from ${from.kind} ${from.id}]`;\n return typeof input === \"string\"\n ? `${label}\\n${input}`\n : [{ type: \"text\" as const, text: label }, ...input];\n}\n\n/**\n * The log projected into what a provider takes.\n *\n * Built fresh for every request and never held. That is the whole reason there\n * is no second transcript to drift from the log, and why nothing needs a test\n * proving two representations agree.\n */\nexport function toMessages(input: {\n instructions: Content;\n entries: readonly Stored[];\n context?: { run?: string; turn?: string };\n /**\n * Name each arrival's sender in the turn text, as `[from kind id]`.\n *\n * Off by default. `from` is provenance the log keeps whatever this says; an\n * application that renders its own attribution into the input it delivers\n * would otherwise hand the model two names for one sender, one of them a\n * session id that means nothing to it.\n */\n attribution?: boolean;\n}): readonly Message[] {\n const messages: Message[] = [{ role: \"system\", content: input.instructions }];\n\n // Run-scoped context sits immediately after the instructions, inside the\n // cacheable prefix, because it does not change for the life of the run.\n if (input.context?.run) messages.push({ role: \"system\", content: input.context.run });\n\n // A summary folds everything up to `replaces`. The source entries stay in the\n // log — compaction changes what the model sees, never what happened.\n const cut = foldedThrough(input.entries);\n\n // A call whose result never committed. It happens when an activation ends\n // between asking and answering — cancelled, interrupted, or beaten to the\n // commit by another worker. The projection has to close it: a provider\n // rejects an assistant turn holding a call with no result, so leaving the gap\n // would make the session permanently unusable. What is said is what is known\n // — the call did not report back — and never an invented result.\n const answered = new Set(\n input.entries.filter((entry) => entry.type === \"tool.finished\").map((entry) => entry.callId),\n );\n\n for (const entry of input.entries) {\n // A summary folds like anything else it covers. Exempting the whole type\n // kept every summary ever written, so a long session carried a chain of\n // them whose content was already inside the newest — duplicated, and\n // rewriting the cached prefix each time one was added.\n if (entry.seq <= cut) continue;\n switch (entry.type) {\n case \"run.started\":\n messages.push({\n role: \"user\",\n content: input.attribution ? labelled(entry.input, entry.from) : entry.input,\n });\n break;\n case \"assistant\": {\n messages.push({\n role: \"assistant\",\n content: entry.content,\n ...(entry.calls?.length ? { calls: entry.calls } : {}),\n });\n for (const call of entry.calls ?? []) {\n if (answered.has(call.callId)) continue;\n messages.push({\n role: \"tool\", callId: call.callId, isError: true,\n content: \"This call did not report back: the activation ended before its result was committed.\",\n });\n }\n break;\n }\n case \"tool.finished\":\n messages.push({\n role: \"tool\",\n callId: entry.callId,\n content: entry.result.content,\n ...(entry.result.isError ? { isError: true } : {}),\n });\n break;\n case \"summary\":\n // Delimited user context rather than an assistant turn: attributing a\n // summary to the assistant puts words in the model's mouth it never said.\n messages.push({ role: \"user\", content: `<summary>\\n${entry.content}\\n</summary>` });\n break;\n // Not model-visible: tool.started is bookkeeping and run.finished is a\n // boundary.\n case \"tool.started\":\n case \"run.finished\":\n break;\n }\n }\n\n // Turn-scoped context sits last, after the cache boundary, because it is for\n // this request only and must not be written into the cached prefix.\n if (input.context?.turn) messages.push({ role: \"system\", content: input.context.turn });\n return messages;\n}\n\n"]}
@@ -1,5 +1,5 @@
1
1
  import { err, ok } from "../../result.js";
2
- const schema = `
2
+ const tables = `
3
3
  CREATE TABLE IF NOT EXISTS sessions (
4
4
  id TEXT PRIMARY KEY,
5
5
  agent_id TEXT NOT NULL,
@@ -25,6 +25,20 @@ const schema = `
25
25
  at TEXT NOT NULL,
26
26
  PRIMARY KEY (session_id, sender, delivery_id)
27
27
  );
28
+ `;
29
+ /**
30
+ * Columns added after a table had already shipped.
31
+ *
32
+ * `CREATE TABLE IF NOT EXISTS` is a no-op against a table that exists, so a
33
+ * column added later never appears in a database made by an older build — and
34
+ * the first index over it fails with a raw `SQLiteError` from inside `bun:sqlite`,
35
+ * which is no way to meet a store whose whole promise is that the log survives.
36
+ * Additive only: a column is added, nothing is dropped, rewritten or lost.
37
+ */
38
+ const added = [
39
+ { table: "sessions", column: "running_since", type: "TEXT" },
40
+ ];
41
+ const indexes = `
28
42
  CREATE INDEX IF NOT EXISTS sessions_by_key ON sessions (key, updated_at DESC, id DESC);
29
43
  CREATE INDEX IF NOT EXISTS sessions_runnable ON sessions (updated_at) WHERE pending <> '[]';
30
44
  DROP INDEX IF EXISTS sessions_stranded; -- sessions_interrupted under its former name
@@ -59,7 +73,16 @@ export function createSqliteStore(input) {
59
73
  const database = input.database;
60
74
  const claimMs = input.claimMs ?? DEFAULT_CLAIM_MS;
61
75
  const deliveryMemoryMs = input.deliveryMemoryMs ?? DEFAULT_DELIVERY_MEMORY_MS;
62
- database.exec(schema);
76
+ // Tables, then the columns an older build's tables lack, then the indexes —
77
+ // which is the only order that works, because an index may be over a column
78
+ // the migration is about to add.
79
+ database.exec(tables);
80
+ for (const { table, column, type } of added) {
81
+ const present = database.query(`SELECT 1 FROM pragma_table_info(?) WHERE name = ?`).all(table, column);
82
+ if (!present.length)
83
+ database.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
84
+ }
85
+ database.exec(indexes);
63
86
  const one = (sql, parameters) => database.query(sql).all(...parameters)[0];
64
87
  const run = (sql, parameters) => { database.query(sql).run(...parameters); };
65
88
  const notFound = (sessionId) => ({ code: "not-found", message: `No session ${sessionId}`, retryable: false });
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../../src/store/adapters/sqlite.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n Store, StoreChange, StoreError, StoreConflict, SessionRead, SessionSummary, Runnable,\n} from \"../store.js\";\nimport type { Delivery, Entry, Stored } from \"../../session/entry.js\";\nimport type { JsonValue } from \"../../json.js\";\nimport { err, ok } from \"../../result.js\";\n\n/**\n * The minimum a driver must provide. Declared here rather than importing one so\n * the same file serves `node:sqlite` and `bun:sqlite`, and so an application can\n * hand in a database it already opened — which is what lets its own tables live\n * beside the log and join against it.\n */\nexport interface SqliteDatabase {\n exec(sql: string): void;\n query(sql: string): { all(...parameters: unknown[]): unknown[]; run(...parameters: unknown[]): unknown };\n}\n\nconst schema = `\n CREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n agent_ver TEXT NOT NULL,\n key TEXT,\n metadata TEXT NOT NULL DEFAULT '{}',\n pending TEXT NOT NULL DEFAULT '[]',\n last_seq INTEGER NOT NULL DEFAULT 0,\n running_since TEXT,\n updated_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS entries (\n session_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n at TEXT NOT NULL,\n body TEXT NOT NULL,\n PRIMARY KEY (session_id, seq)\n );\n CREATE TABLE IF NOT EXISTS deliveries (\n session_id TEXT NOT NULL,\n sender TEXT NOT NULL,\n delivery_id TEXT NOT NULL,\n at TEXT NOT NULL,\n PRIMARY KEY (session_id, sender, delivery_id)\n );\n CREATE INDEX IF NOT EXISTS sessions_by_key ON sessions (key, updated_at DESC, id DESC);\n CREATE INDEX IF NOT EXISTS sessions_runnable ON sessions (updated_at) WHERE pending <> '[]';\n DROP INDEX IF EXISTS sessions_stranded; -- sessions_interrupted under its former name\n CREATE INDEX IF NOT EXISTS sessions_interrupted ON sessions (updated_at) WHERE running_since IS NOT NULL;\n CREATE INDEX IF NOT EXISTS deliveries_by_age ON deliveries (at);\n`;\n\ninterface SessionRow {\n id: string; agent_id: string; agent_ver: string; key: string | null;\n metadata: string; pending: string; last_seq: number; running_since: string | null; updated_at: string;\n}\n\n/** `updatedAt` and the session id, so a page boundary is a point and not an instant. */\nconst splitCursor = (cursor: string): [string, string] => {\n const cut = cursor.lastIndexOf(\"|\");\n return cut < 0 ? [cursor, \"\"] : [cursor.slice(0, cut), cursor.slice(cut + 1)];\n};\n\n/**\n * How long a claim holds before another worker may take the session.\n *\n * Long enough that an ordinary turn finishes inside it, short enough that a\n * worker killed mid-turn does not strand its session for an afternoon. If the\n * original worker is somehow still alive when it expires, its writes fail the\n * compare-and-swap, so the window governs wasted work and never correctness.\n */\nconst DEFAULT_CLAIM_MS = 10 * 60 * 1000;\n\n/**\n * How long a delivery id is remembered.\n *\n * `id` means \"this delivery, once\" — a webhook redelivered ten seconds later,\n * a retried send, a cron that fired twice all collapse to one. That has to\n * outlive the queue, because the common case is a duplicate arriving after the\n * first copy was already read. It cannot be remembered forever, so it is\n * remembered for a window an application can set and reason about.\n */\nconst DEFAULT_DELIVERY_MEMORY_MS = 7 * 24 * 60 * 60 * 1000;\n\nexport function createSqliteStore(input: {\n database: SqliteDatabase;\n claimMs?: number;\n /** How long a delivery id is remembered. Defaults to seven days. */\n deliveryMemoryMs?: number;\n}): Store {\n const database = input.database;\n const claimMs = input.claimMs ?? DEFAULT_CLAIM_MS;\n const deliveryMemoryMs = input.deliveryMemoryMs ?? DEFAULT_DELIVERY_MEMORY_MS;\n database.exec(schema);\n\n const one = <T>(sql: string, parameters: readonly unknown[]): T | undefined =>\n database.query(sql).all(...parameters)[0] as T | undefined;\n const run = (sql: string, parameters: readonly unknown[]) => { database.query(sql).run(...parameters); };\n const notFound = (sessionId: string): StoreError =>\n ({ code: \"not-found\", message: `No session ${sessionId}`, retryable: false });\n const failed = (error: unknown): StoreError =>\n ({ code: \"failed\", message: error instanceof Error ? error.message : String(error), retryable: false });\n const queueOf = (row: { pending: string }): Delivery[] => JSON.parse(row.pending) as Delivery[];\n\n /**\n * Watchers, woken after a write is committed.\n *\n * Every call site is already past its `COMMIT`, which is what makes the change\n * readable. The microtask is for the other half: a watcher that throws must\n * not fail the write that woke it — it surfaces as the caller's own unhandled\n * error rather than as a refused append — and one that writes back through\n * this store must not do so from inside the stack of the write it is\n * answering.\n */\n const watchers = new Set<(change: StoreChange) => void>();\n const woke = (changes: readonly StoreChange[]): void => {\n if (!watchers.size || !changes.length) return;\n for (const watcher of [...watchers]) {\n // One microtask each. A watcher that throws must not fail the write that\n // woke it, and must not suppress the watchers after it in the loop — in\n // its own task the throw surfaces as the caller's unhandled error and\n // costs nobody else their wake. Membership is re-checked at delivery, so\n // a watcher that let go between the write and this task is not called:\n // `off()` says stop, and a queued wake is still a wake.\n queueMicrotask(() => {\n if (!watchers.has(watcher)) return;\n for (const change of changes) watcher(change);\n });\n }\n };\n\n return {\n /** Idempotent: a session that is already here keeps what it was created with. */\n async create({ sessionId, agent, key, metadata }) {\n try {\n const existing = one<{ id: string }>(`SELECT id FROM sessions WHERE id = ?`, [sessionId]);\n if (existing) return ok(undefined);\n run(\n `INSERT OR IGNORE INTO sessions (id, agent_id, agent_ver, key, metadata, updated_at)\n VALUES (?, ?, ?, ?, ?, ?)`,\n [sessionId, agent.id, agent.version, key ?? null, JSON.stringify(metadata ?? {}), new Date().toISOString()],\n );\n // Only a session that really appeared is a change, and a new one has\n // nothing waiting on it — announcing `runnable: false` for a session\n // that already had input would have been wrong rather than spurious.\n woke([{ sessionId, runnable: false }]);\n return ok(undefined);\n } catch (error) { return err(failed(error)); }\n },\n\n async read({ sessionId, afterSeq = 0 }) {\n try {\n const session = one<SessionRow>(`SELECT * FROM sessions WHERE id = ?`, [sessionId]);\n if (!session) return err(notFound(sessionId));\n const rows = database.query(\n `SELECT seq, at, body FROM entries WHERE session_id = ? AND seq > ? ORDER BY seq`,\n ).all(sessionId, afterSeq) as { seq: number; at: string; body: string }[];\n return ok<SessionRead>({\n sessionId,\n agent: { id: session.agent_id, version: session.agent_ver },\n seq: session.last_seq,\n metadata: JSON.parse(session.metadata) as JsonValue,\n entries: rows.map((row): Stored => ({ ...(JSON.parse(row.body) as Entry), seq: row.seq, at: row.at })),\n pending: queueOf(session),\n });\n } catch (error) { return err(failed(error)); }\n },\n\n /**\n * Entries, deliveries, metadata and the queue in one transaction.\n *\n * The compare-and-swap on `last_seq` decides everything else: a caller\n * working from a stale position loses and is told the real one, and because\n * the queue is consumed here rather than in `next`, a loser leaves the\n * messages it was carrying untouched.\n */\n async append({ sessionId, expectedSeq, entries, enqueue, metadata, takePending }) {\n const at = new Date().toISOString();\n try {\n // Inside the try: on a busy database this is where the failure lands,\n // and a throw here escaped the `Result` the port promises.\n database.exec(\"BEGIN IMMEDIATE\");\n const session = one<SessionRow>(`SELECT * FROM sessions WHERE id = ?`, [sessionId]);\n if (!session) { database.exec(\"ROLLBACK\"); return err(notFound(sessionId)); }\n if (session.last_seq !== expectedSeq) {\n database.exec(\"ROLLBACK\");\n return err<StoreConflict>({\n code: \"conflict\", message: `Session moved to ${session.last_seq}`,\n retryable: true, actualSeq: session.last_seq,\n });\n }\n entries.forEach((entry, offset) => {\n run(`INSERT INTO entries (session_id, seq, at, body) VALUES (?, ?, ?, ?)`,\n [sessionId, expectedSeq + offset + 1, at, JSON.stringify(entry)]);\n });\n\n // An activation is open from the entry that starts it to the entry that\n // ends it. `next` reads this to keep a second one from beginning, and\n // the log is the only thing that knows — so it is derived from the log\n // rather than asserted by a caller.\n const opens = entries.some((entry) => entry.type === \"run.started\");\n const closes = entries.some((entry) => entry.type === \"run.finished\");\n // Only a write that records work renews the claim. Renewing on any\n // append at all meant a message *to* a session renewed the claim of the\n // worker that was supposed to read it — so a dead worker's session\n // stayed claimed for as long as anyone kept talking to it, invisible to\n // `next` (the claim never lapsed) and to `interrupted` (the queue was not\n // empty). An activation renews by doing its work, and being spoken to\n // is not its work.\n const worked = entries.length > 0;\n // Every write by an open activation renews its claim. Without this the\n // timestamp stayed at whenever the run began, so any turn outstanding\n // longer than the claim window was handed to a second worker while it\n // was still going — and a compare-and-swap cannot un-run a tool call.\n // A worker that dies still releases, because a dead worker stops writing.\n const runningSince = closes ? null : (opens || (worked && session.running_since)) ? at : session.running_since;\n\n const merged = metadata\n ? JSON.stringify({ ...(JSON.parse(session.metadata) as object), ...(metadata as object) })\n : session.metadata;\n const waiting = queueOf(session);\n if (takePending !== undefined && takePending > waiting.length) {\n // The one loss this design exists to prevent, reachable from a\n // caller's off-by-one: silently dropping messages nothing recorded.\n // `takePending` is only ever `claim.pending.length`, so a larger\n // number means the caller is working from a queue that is not there.\n database.exec(\"ROLLBACK\");\n return err<StoreError>({\n code: \"conflict\",\n message: `Asked to take ${takePending} deliveries from a queue of ${waiting.length}`,\n retryable: false,\n });\n }\n const queue = takePending ? waiting.slice(takePending) : waiting;\n /** Which sessions this write put input on, this one included. */\n const delivered = new Set<string>();\n run(\n `UPDATE sessions SET last_seq = ?, metadata = ?, pending = ?, running_since = ?, updated_at = ?\n WHERE id = ?`,\n [expectedSeq + entries.length, merged, JSON.stringify(queue), runningSince, at, sessionId],\n );\n\n // Swept before the ids are checked, not after: an id past its window is\n // forgotten, and one still inside it is not. Sweeping afterwards left\n // every expired id alive for exactly one more send.\n run(`DELETE FROM deliveries WHERE at < ?`,\n [new Date(Date.parse(at) - deliveryMemoryMs).toISOString()]);\n\n for (const delivery of enqueue ?? []) {\n const target = one<{ pending: string }>(`SELECT pending FROM sessions WHERE id = ?`, [delivery.sessionId]);\n if (!target) { database.exec(\"ROLLBACK\"); return err(notFound(delivery.sessionId)); }\n\n // Two deliveries with the same id from the same sender are the same\n // delivery, and stay so after the first has been read — the whole\n // point of an idempotency key is that it survives the queue. The\n // sender is part of it because the id is the sender's own: without\n // that, two agents numbering their messages from one silently\n // suppress each other.\n // The column is one string, so the two fields are joined into one\n // here. The encoding is this adapter's own and never leaves it: no\n // caller reads the column, and a store that keys the same fact some\n // other way is answering the same question, which is the one the\n // conformance suite asks.\n if (delivery.id) {\n const sender = delivery.from ? `${delivery.from.kind}:${delivery.from.id}` : \"\";\n const seen = one<{ delivery_id: string }>(\n `SELECT delivery_id FROM deliveries WHERE session_id = ? AND sender = ? AND delivery_id = ?`,\n [delivery.sessionId, sender, delivery.id]);\n if (seen) continue;\n run(`INSERT INTO deliveries (session_id, sender, delivery_id, at) VALUES (?, ?, ?, ?)`,\n [delivery.sessionId, sender, delivery.id, at]);\n }\n\n run(`UPDATE sessions SET pending = ?, updated_at = ? WHERE id = ?`,\n [JSON.stringify([...queueOf(target), delivery]), at, delivery.sessionId]);\n delivered.add(delivery.sessionId);\n }\n database.exec(\"COMMIT\");\n // After the commit, never before: a watcher that looked on an earlier\n // wake would find nothing and never be told again.\n woke([\n { sessionId, runnable: delivered.has(sessionId) || queue.length > 0 },\n ...[...delivered].filter((id) => id !== sessionId).map((id) => ({ sessionId: id, runnable: true })),\n ]);\n return ok({ seq: expectedSeq + entries.length });\n } catch (error) {\n database.exec(\"ROLLBACK\");\n return err(failed(error));\n }\n },\n\n async list({ key, limit = 50, before }) {\n try {\n // The cursor carries the id as well as the timestamp, because sessions\n // written in the same tick have no order without it — and a plain\n // `< before` then steps over every row that shares the boundary instant.\n const [beforeAt, beforeId] = before ? splitCursor(before) : [null, null];\n const rows = database.query(\n `SELECT * FROM sessions\n WHERE (? IS NULL OR key = ?)\n AND (? IS NULL OR updated_at < ? OR (updated_at = ? AND id < ?))\n ORDER BY updated_at DESC, id DESC LIMIT ?`,\n ).all(key ?? null, key ?? null, beforeAt, beforeAt, beforeAt, beforeId, limit) as SessionRow[];\n return ok(rows.map((row): SessionSummary => ({\n sessionId: row.id,\n agent: { id: row.agent_id, version: row.agent_ver },\n key: row.key,\n seq: row.last_seq,\n metadata: JSON.parse(row.metadata) as JsonValue,\n updatedAt: row.updated_at,\n })));\n } catch (error) { return err(failed(error)); }\n },\n\n /**\n * Claim a session that has input and no activation already running.\n *\n * The queue is reported and left in place; the append that commits it as\n * entries is what removes it. Nothing here is destructive, so a worker that\n * takes this and then dies costs one expired claim and no messages.\n */\n async next({ signal }) {\n // Nothing is owed to a caller that has given up. Answered rather than\n // refused: \"nothing to claim\" is true, and it needs no code of its own.\n if (signal?.aborted) return ok(undefined);\n const now = Date.now();\n const stale = new Date(now - claimMs).toISOString();\n try {\n database.exec(\"BEGIN IMMEDIATE\");\n const row = one<SessionRow>(\n `SELECT * FROM sessions\n WHERE pending <> '[]' AND (running_since IS NULL OR running_since < ?)\n ORDER BY updated_at LIMIT 1`, [stale]);\n if (!row) { database.exec(\"COMMIT\"); return ok(undefined); }\n run(`UPDATE sessions SET running_since = ? WHERE id = ?`, [new Date(now).toISOString(), row.id]);\n database.exec(\"COMMIT\");\n return ok<Runnable>({\n sessionId: row.id,\n seq: row.last_seq,\n pending: queueOf(row),\n metadata: JSON.parse(row.metadata) as JsonValue,\n });\n } catch (error) {\n database.exec(\"ROLLBACK\");\n return err(failed(error));\n }\n },\n\n /**\n * Claim a session whose activation was interrupted.\n *\n * The mirror of `next`, over the two columns that already exist: an open\n * run — `running_since` set, because the entry that would clear it never\n * committed — whose claim has lapsed, and an empty queue, because the\n * commit that opened the run took what made it runnable. Nothing else in\n * the store can see this session, which is why the port asks for it here\n * rather than leaving every application to write this query itself.\n */\n async interrupted({ signal }) {\n if (signal?.aborted) return ok(undefined);\n const now = Date.now();\n const stale = new Date(now - claimMs).toISOString();\n try {\n database.exec(\"BEGIN IMMEDIATE\");\n const row = one<SessionRow>(\n `SELECT * FROM sessions\n WHERE pending = '[]' AND running_since IS NOT NULL AND running_since < ?\n ORDER BY updated_at LIMIT 1`, [stale]);\n if (!row) { database.exec(\"COMMIT\"); return ok(undefined); }\n run(`UPDATE sessions SET running_since = ? WHERE id = ?`, [new Date(now).toISOString(), row.id]);\n database.exec(\"COMMIT\");\n return ok<Runnable>({\n sessionId: row.id,\n seq: row.last_seq,\n pending: [],\n metadata: JSON.parse(row.metadata) as JsonValue,\n });\n } catch (error) {\n database.exec(\"ROLLBACK\");\n return err(failed(error));\n }\n },\n\n /**\n * The feed is this store's own writes, and reaches exactly as far as they\n * do: a watcher here is woken by what this instance commits, and by nothing\n * else. Another process on the same file, a second store over the same\n * handle, and the application's own SQL beside the log are all invisible,\n * because sqlite has no channel to tell us — that is the miss the port\n * requires a heartbeat for. The case that matters is covered: the surface\n * that writes a delivery and the worker that reads it are the same process,\n * and usually the same store.\n */\n watch(watcher) {\n watchers.add(watcher);\n return () => { watchers.delete(watcher); };\n },\n\n async close() { watchers.clear(); /* the caller owns the handle it passed in */ },\n };\n}\n"]}
1
+ {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../../src/store/adapters/sqlite.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n Store, StoreChange, StoreError, StoreConflict, SessionRead, SessionSummary, Runnable,\n} from \"../store.js\";\nimport type { Delivery, Entry, Stored } from \"../../session/entry.js\";\nimport type { JsonValue } from \"../../json.js\";\nimport { err, ok } from \"../../result.js\";\n\n/**\n * The minimum a driver must provide. Declared here rather than importing one so\n * the same file serves `node:sqlite` and `bun:sqlite`, and so an application can\n * hand in a database it already opened — which is what lets its own tables live\n * beside the log and join against it.\n */\nexport interface SqliteDatabase {\n exec(sql: string): void;\n query(sql: string): { all(...parameters: unknown[]): unknown[]; run(...parameters: unknown[]): unknown };\n}\n\nconst tables = `\n CREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n agent_ver TEXT NOT NULL,\n key TEXT,\n metadata TEXT NOT NULL DEFAULT '{}',\n pending TEXT NOT NULL DEFAULT '[]',\n last_seq INTEGER NOT NULL DEFAULT 0,\n running_since TEXT,\n updated_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS entries (\n session_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n at TEXT NOT NULL,\n body TEXT NOT NULL,\n PRIMARY KEY (session_id, seq)\n );\n CREATE TABLE IF NOT EXISTS deliveries (\n session_id TEXT NOT NULL,\n sender TEXT NOT NULL,\n delivery_id TEXT NOT NULL,\n at TEXT NOT NULL,\n PRIMARY KEY (session_id, sender, delivery_id)\n );\n`;\n\n/**\n * Columns added after a table had already shipped.\n *\n * `CREATE TABLE IF NOT EXISTS` is a no-op against a table that exists, so a\n * column added later never appears in a database made by an older build — and\n * the first index over it fails with a raw `SQLiteError` from inside `bun:sqlite`,\n * which is no way to meet a store whose whole promise is that the log survives.\n * Additive only: a column is added, nothing is dropped, rewritten or lost.\n */\nconst added: readonly { table: string; column: string; type: string }[] = [\n { table: \"sessions\", column: \"running_since\", type: \"TEXT\" },\n];\n\nconst indexes = `\n CREATE INDEX IF NOT EXISTS sessions_by_key ON sessions (key, updated_at DESC, id DESC);\n CREATE INDEX IF NOT EXISTS sessions_runnable ON sessions (updated_at) WHERE pending <> '[]';\n DROP INDEX IF EXISTS sessions_stranded; -- sessions_interrupted under its former name\n CREATE INDEX IF NOT EXISTS sessions_interrupted ON sessions (updated_at) WHERE running_since IS NOT NULL;\n CREATE INDEX IF NOT EXISTS deliveries_by_age ON deliveries (at);\n`;\n\ninterface SessionRow {\n id: string; agent_id: string; agent_ver: string; key: string | null;\n metadata: string; pending: string; last_seq: number; running_since: string | null; updated_at: string;\n}\n\n/** `updatedAt` and the session id, so a page boundary is a point and not an instant. */\nconst splitCursor = (cursor: string): [string, string] => {\n const cut = cursor.lastIndexOf(\"|\");\n return cut < 0 ? [cursor, \"\"] : [cursor.slice(0, cut), cursor.slice(cut + 1)];\n};\n\n/**\n * How long a claim holds before another worker may take the session.\n *\n * Long enough that an ordinary turn finishes inside it, short enough that a\n * worker killed mid-turn does not strand its session for an afternoon. If the\n * original worker is somehow still alive when it expires, its writes fail the\n * compare-and-swap, so the window governs wasted work and never correctness.\n */\nconst DEFAULT_CLAIM_MS = 10 * 60 * 1000;\n\n/**\n * How long a delivery id is remembered.\n *\n * `id` means \"this delivery, once\" — a webhook redelivered ten seconds later,\n * a retried send, a cron that fired twice all collapse to one. That has to\n * outlive the queue, because the common case is a duplicate arriving after the\n * first copy was already read. It cannot be remembered forever, so it is\n * remembered for a window an application can set and reason about.\n */\nconst DEFAULT_DELIVERY_MEMORY_MS = 7 * 24 * 60 * 60 * 1000;\n\nexport function createSqliteStore(input: {\n database: SqliteDatabase;\n claimMs?: number;\n /** How long a delivery id is remembered. Defaults to seven days. */\n deliveryMemoryMs?: number;\n}): Store {\n const database = input.database;\n const claimMs = input.claimMs ?? DEFAULT_CLAIM_MS;\n const deliveryMemoryMs = input.deliveryMemoryMs ?? DEFAULT_DELIVERY_MEMORY_MS;\n\n // Tables, then the columns an older build's tables lack, then the indexes —\n // which is the only order that works, because an index may be over a column\n // the migration is about to add.\n database.exec(tables);\n for (const { table, column, type } of added) {\n const present = database.query(`SELECT 1 FROM pragma_table_info(?) WHERE name = ?`).all(table, column);\n if (!present.length) database.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);\n }\n database.exec(indexes);\n\n const one = <T>(sql: string, parameters: readonly unknown[]): T | undefined =>\n database.query(sql).all(...parameters)[0] as T | undefined;\n const run = (sql: string, parameters: readonly unknown[]) => { database.query(sql).run(...parameters); };\n const notFound = (sessionId: string): StoreError =>\n ({ code: \"not-found\", message: `No session ${sessionId}`, retryable: false });\n const failed = (error: unknown): StoreError =>\n ({ code: \"failed\", message: error instanceof Error ? error.message : String(error), retryable: false });\n const queueOf = (row: { pending: string }): Delivery[] => JSON.parse(row.pending) as Delivery[];\n\n /**\n * Watchers, woken after a write is committed.\n *\n * Every call site is already past its `COMMIT`, which is what makes the change\n * readable. The microtask is for the other half: a watcher that throws must\n * not fail the write that woke it — it surfaces as the caller's own unhandled\n * error rather than as a refused append — and one that writes back through\n * this store must not do so from inside the stack of the write it is\n * answering.\n */\n const watchers = new Set<(change: StoreChange) => void>();\n const woke = (changes: readonly StoreChange[]): void => {\n if (!watchers.size || !changes.length) return;\n for (const watcher of [...watchers]) {\n // One microtask each. A watcher that throws must not fail the write that\n // woke it, and must not suppress the watchers after it in the loop — in\n // its own task the throw surfaces as the caller's unhandled error and\n // costs nobody else their wake. Membership is re-checked at delivery, so\n // a watcher that let go between the write and this task is not called:\n // `off()` says stop, and a queued wake is still a wake.\n queueMicrotask(() => {\n if (!watchers.has(watcher)) return;\n for (const change of changes) watcher(change);\n });\n }\n };\n\n return {\n /** Idempotent: a session that is already here keeps what it was created with. */\n async create({ sessionId, agent, key, metadata }) {\n try {\n const existing = one<{ id: string }>(`SELECT id FROM sessions WHERE id = ?`, [sessionId]);\n if (existing) return ok(undefined);\n run(\n `INSERT OR IGNORE INTO sessions (id, agent_id, agent_ver, key, metadata, updated_at)\n VALUES (?, ?, ?, ?, ?, ?)`,\n [sessionId, agent.id, agent.version, key ?? null, JSON.stringify(metadata ?? {}), new Date().toISOString()],\n );\n // Only a session that really appeared is a change, and a new one has\n // nothing waiting on it — announcing `runnable: false` for a session\n // that already had input would have been wrong rather than spurious.\n woke([{ sessionId, runnable: false }]);\n return ok(undefined);\n } catch (error) { return err(failed(error)); }\n },\n\n async read({ sessionId, afterSeq = 0 }) {\n try {\n const session = one<SessionRow>(`SELECT * FROM sessions WHERE id = ?`, [sessionId]);\n if (!session) return err(notFound(sessionId));\n const rows = database.query(\n `SELECT seq, at, body FROM entries WHERE session_id = ? AND seq > ? ORDER BY seq`,\n ).all(sessionId, afterSeq) as { seq: number; at: string; body: string }[];\n return ok<SessionRead>({\n sessionId,\n agent: { id: session.agent_id, version: session.agent_ver },\n seq: session.last_seq,\n metadata: JSON.parse(session.metadata) as JsonValue,\n entries: rows.map((row): Stored => ({ ...(JSON.parse(row.body) as Entry), seq: row.seq, at: row.at })),\n pending: queueOf(session),\n });\n } catch (error) { return err(failed(error)); }\n },\n\n /**\n * Entries, deliveries, metadata and the queue in one transaction.\n *\n * The compare-and-swap on `last_seq` decides everything else: a caller\n * working from a stale position loses and is told the real one, and because\n * the queue is consumed here rather than in `next`, a loser leaves the\n * messages it was carrying untouched.\n */\n async append({ sessionId, expectedSeq, entries, enqueue, metadata, takePending }) {\n const at = new Date().toISOString();\n try {\n // Inside the try: on a busy database this is where the failure lands,\n // and a throw here escaped the `Result` the port promises.\n database.exec(\"BEGIN IMMEDIATE\");\n const session = one<SessionRow>(`SELECT * FROM sessions WHERE id = ?`, [sessionId]);\n if (!session) { database.exec(\"ROLLBACK\"); return err(notFound(sessionId)); }\n if (session.last_seq !== expectedSeq) {\n database.exec(\"ROLLBACK\");\n return err<StoreConflict>({\n code: \"conflict\", message: `Session moved to ${session.last_seq}`,\n retryable: true, actualSeq: session.last_seq,\n });\n }\n entries.forEach((entry, offset) => {\n run(`INSERT INTO entries (session_id, seq, at, body) VALUES (?, ?, ?, ?)`,\n [sessionId, expectedSeq + offset + 1, at, JSON.stringify(entry)]);\n });\n\n // An activation is open from the entry that starts it to the entry that\n // ends it. `next` reads this to keep a second one from beginning, and\n // the log is the only thing that knows — so it is derived from the log\n // rather than asserted by a caller.\n const opens = entries.some((entry) => entry.type === \"run.started\");\n const closes = entries.some((entry) => entry.type === \"run.finished\");\n // Only a write that records work renews the claim. Renewing on any\n // append at all meant a message *to* a session renewed the claim of the\n // worker that was supposed to read it — so a dead worker's session\n // stayed claimed for as long as anyone kept talking to it, invisible to\n // `next` (the claim never lapsed) and to `interrupted` (the queue was not\n // empty). An activation renews by doing its work, and being spoken to\n // is not its work.\n const worked = entries.length > 0;\n // Every write by an open activation renews its claim. Without this the\n // timestamp stayed at whenever the run began, so any turn outstanding\n // longer than the claim window was handed to a second worker while it\n // was still going — and a compare-and-swap cannot un-run a tool call.\n // A worker that dies still releases, because a dead worker stops writing.\n const runningSince = closes ? null : (opens || (worked && session.running_since)) ? at : session.running_since;\n\n const merged = metadata\n ? JSON.stringify({ ...(JSON.parse(session.metadata) as object), ...(metadata as object) })\n : session.metadata;\n const waiting = queueOf(session);\n if (takePending !== undefined && takePending > waiting.length) {\n // The one loss this design exists to prevent, reachable from a\n // caller's off-by-one: silently dropping messages nothing recorded.\n // `takePending` is only ever `claim.pending.length`, so a larger\n // number means the caller is working from a queue that is not there.\n database.exec(\"ROLLBACK\");\n return err<StoreError>({\n code: \"conflict\",\n message: `Asked to take ${takePending} deliveries from a queue of ${waiting.length}`,\n retryable: false,\n });\n }\n const queue = takePending ? waiting.slice(takePending) : waiting;\n /** Which sessions this write put input on, this one included. */\n const delivered = new Set<string>();\n run(\n `UPDATE sessions SET last_seq = ?, metadata = ?, pending = ?, running_since = ?, updated_at = ?\n WHERE id = ?`,\n [expectedSeq + entries.length, merged, JSON.stringify(queue), runningSince, at, sessionId],\n );\n\n // Swept before the ids are checked, not after: an id past its window is\n // forgotten, and one still inside it is not. Sweeping afterwards left\n // every expired id alive for exactly one more send.\n run(`DELETE FROM deliveries WHERE at < ?`,\n [new Date(Date.parse(at) - deliveryMemoryMs).toISOString()]);\n\n for (const delivery of enqueue ?? []) {\n const target = one<{ pending: string }>(`SELECT pending FROM sessions WHERE id = ?`, [delivery.sessionId]);\n if (!target) { database.exec(\"ROLLBACK\"); return err(notFound(delivery.sessionId)); }\n\n // Two deliveries with the same id from the same sender are the same\n // delivery, and stay so after the first has been read — the whole\n // point of an idempotency key is that it survives the queue. The\n // sender is part of it because the id is the sender's own: without\n // that, two agents numbering their messages from one silently\n // suppress each other.\n // The column is one string, so the two fields are joined into one\n // here. The encoding is this adapter's own and never leaves it: no\n // caller reads the column, and a store that keys the same fact some\n // other way is answering the same question, which is the one the\n // conformance suite asks.\n if (delivery.id) {\n const sender = delivery.from ? `${delivery.from.kind}:${delivery.from.id}` : \"\";\n const seen = one<{ delivery_id: string }>(\n `SELECT delivery_id FROM deliveries WHERE session_id = ? AND sender = ? AND delivery_id = ?`,\n [delivery.sessionId, sender, delivery.id]);\n if (seen) continue;\n run(`INSERT INTO deliveries (session_id, sender, delivery_id, at) VALUES (?, ?, ?, ?)`,\n [delivery.sessionId, sender, delivery.id, at]);\n }\n\n run(`UPDATE sessions SET pending = ?, updated_at = ? WHERE id = ?`,\n [JSON.stringify([...queueOf(target), delivery]), at, delivery.sessionId]);\n delivered.add(delivery.sessionId);\n }\n database.exec(\"COMMIT\");\n // After the commit, never before: a watcher that looked on an earlier\n // wake would find nothing and never be told again.\n woke([\n { sessionId, runnable: delivered.has(sessionId) || queue.length > 0 },\n ...[...delivered].filter((id) => id !== sessionId).map((id) => ({ sessionId: id, runnable: true })),\n ]);\n return ok({ seq: expectedSeq + entries.length });\n } catch (error) {\n database.exec(\"ROLLBACK\");\n return err(failed(error));\n }\n },\n\n async list({ key, limit = 50, before }) {\n try {\n // The cursor carries the id as well as the timestamp, because sessions\n // written in the same tick have no order without it — and a plain\n // `< before` then steps over every row that shares the boundary instant.\n const [beforeAt, beforeId] = before ? splitCursor(before) : [null, null];\n const rows = database.query(\n `SELECT * FROM sessions\n WHERE (? IS NULL OR key = ?)\n AND (? IS NULL OR updated_at < ? OR (updated_at = ? AND id < ?))\n ORDER BY updated_at DESC, id DESC LIMIT ?`,\n ).all(key ?? null, key ?? null, beforeAt, beforeAt, beforeAt, beforeId, limit) as SessionRow[];\n return ok(rows.map((row): SessionSummary => ({\n sessionId: row.id,\n agent: { id: row.agent_id, version: row.agent_ver },\n key: row.key,\n seq: row.last_seq,\n metadata: JSON.parse(row.metadata) as JsonValue,\n updatedAt: row.updated_at,\n })));\n } catch (error) { return err(failed(error)); }\n },\n\n /**\n * Claim a session that has input and no activation already running.\n *\n * The queue is reported and left in place; the append that commits it as\n * entries is what removes it. Nothing here is destructive, so a worker that\n * takes this and then dies costs one expired claim and no messages.\n */\n async next({ signal }) {\n // Nothing is owed to a caller that has given up. Answered rather than\n // refused: \"nothing to claim\" is true, and it needs no code of its own.\n if (signal?.aborted) return ok(undefined);\n const now = Date.now();\n const stale = new Date(now - claimMs).toISOString();\n try {\n database.exec(\"BEGIN IMMEDIATE\");\n const row = one<SessionRow>(\n `SELECT * FROM sessions\n WHERE pending <> '[]' AND (running_since IS NULL OR running_since < ?)\n ORDER BY updated_at LIMIT 1`, [stale]);\n if (!row) { database.exec(\"COMMIT\"); return ok(undefined); }\n run(`UPDATE sessions SET running_since = ? WHERE id = ?`, [new Date(now).toISOString(), row.id]);\n database.exec(\"COMMIT\");\n return ok<Runnable>({\n sessionId: row.id,\n seq: row.last_seq,\n pending: queueOf(row),\n metadata: JSON.parse(row.metadata) as JsonValue,\n });\n } catch (error) {\n database.exec(\"ROLLBACK\");\n return err(failed(error));\n }\n },\n\n /**\n * Claim a session whose activation was interrupted.\n *\n * The mirror of `next`, over the two columns that already exist: an open\n * run — `running_since` set, because the entry that would clear it never\n * committed — whose claim has lapsed, and an empty queue, because the\n * commit that opened the run took what made it runnable. Nothing else in\n * the store can see this session, which is why the port asks for it here\n * rather than leaving every application to write this query itself.\n */\n async interrupted({ signal }) {\n if (signal?.aborted) return ok(undefined);\n const now = Date.now();\n const stale = new Date(now - claimMs).toISOString();\n try {\n database.exec(\"BEGIN IMMEDIATE\");\n const row = one<SessionRow>(\n `SELECT * FROM sessions\n WHERE pending = '[]' AND running_since IS NOT NULL AND running_since < ?\n ORDER BY updated_at LIMIT 1`, [stale]);\n if (!row) { database.exec(\"COMMIT\"); return ok(undefined); }\n run(`UPDATE sessions SET running_since = ? WHERE id = ?`, [new Date(now).toISOString(), row.id]);\n database.exec(\"COMMIT\");\n return ok<Runnable>({\n sessionId: row.id,\n seq: row.last_seq,\n pending: [],\n metadata: JSON.parse(row.metadata) as JsonValue,\n });\n } catch (error) {\n database.exec(\"ROLLBACK\");\n return err(failed(error));\n }\n },\n\n /**\n * The feed is this store's own writes, and reaches exactly as far as they\n * do: a watcher here is woken by what this instance commits, and by nothing\n * else. Another process on the same file, a second store over the same\n * handle, and the application's own SQL beside the log are all invisible,\n * because sqlite has no channel to tell us — that is the miss the port\n * requires a heartbeat for. The case that matters is covered: the surface\n * that writes a delivery and the worker that reads it are the same process,\n * and usually the same store.\n */\n watch(watcher) {\n watchers.add(watcher);\n return () => { watchers.delete(watcher); };\n },\n\n async close() { watchers.clear(); /* the caller owns the handle it passed in */ },\n };\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import type { Sink } from "./render.js";
2
+ export interface TurnSource {
3
+ /** Whether a person is waiting, which decides prompting and how a failure ends. */
4
+ interactive: boolean;
5
+ lines: AsyncIterable<string>;
6
+ }
7
+ export declare function turnsFrom(input: {
8
+ /** The task on argv, if there was one. */
9
+ task: string;
10
+ /** Injected so a test never touches a terminal. */
11
+ stdin?: NodeJS.ReadStream;
12
+ isTTY?: boolean;
13
+ }): TurnSource;
14
+ /**
15
+ * The standard pair of channels for a terminal.
16
+ *
17
+ * The answer on stdout, the account of the run on stderr, so redirecting the
18
+ * first captures the answer and nothing else. Written out identically in three
19
+ * recipes before this existed, which is two more copies than a fact deserves.
20
+ */
21
+ export declare function terminalSink(options?: {
22
+ detail?: Sink["detail"];
23
+ }): Sink;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Where a person's turns come from.
3
+ *
4
+ * The inbound half of the one channel this package ships, and `render` is the
5
+ * outbound half. A channel does three things — decide which session a person is
6
+ * talking to, deliver a message exactly once, and put the result in a medium's
7
+ * own shape. For a terminal the first is "the one session" and the second is a
8
+ * pipe that cannot fail, so the two halves collapse to a keyboard and a screen,
9
+ * and both are the same for every application. A mail bridge keeps all three
10
+ * and is yours; this is the degenerate case, which is why it can be here.
11
+ *
12
+ * It is the one file in `src` that touches `process`, and it is confined to
13
+ * that: `render` takes a sink, this takes a stream, and neither reaches for a
14
+ * terminal it was not handed.
15
+ *
16
+ * **Multi-turn is the default, and it is not a flag.** A conversation is what
17
+ * this recipe is for, and one-shot is the special case — the one where nobody
18
+ * is there. So the fact that decides it is whether stdin is a terminal, not
19
+ * something an operator has to remember:
20
+ *
21
+ * - A person at a terminal gets a prompt, and any task on argv is simply
22
+ * their first turn.
23
+ * - A pipe or a redirect gets one shot and an exit code, so
24
+ * `recipe native-agent "…" > answer.txt` and `echo … | recipe` both behave
25
+ * the way every other command-line program does.
26
+ *
27
+ * There is no flag either way. `--once` existed to give a person at a terminal
28
+ * the script behaviour, and `echo "…" | recipe` already does that — a second
29
+ * way to say a thing the shell says better. Prompting into a pipe has no flag
30
+ * for the opposite reason: it would wait for a person who is not there.
31
+ */
32
+ import { createInterface } from "node:readline";
33
+ export function turnsFrom(input) {
34
+ const stdin = input.stdin ?? process.stdin;
35
+ const interactive = input.isTTY ?? stdin.isTTY === true;
36
+ if (!interactive) {
37
+ return {
38
+ interactive: false,
39
+ lines: (async function* () {
40
+ // A task on argv wins; otherwise the whole of stdin is the task, which
41
+ // is what a pipe means.
42
+ if (input.task) {
43
+ yield input.task;
44
+ return;
45
+ }
46
+ const piped = await new Promise((resolve) => {
47
+ let read = "";
48
+ stdin.setEncoding("utf8");
49
+ stdin.on("data", (chunk) => { read += chunk; });
50
+ stdin.on("end", () => resolve(read));
51
+ });
52
+ if (piped.trim())
53
+ yield piped.trim();
54
+ })(),
55
+ };
56
+ }
57
+ return {
58
+ interactive: true,
59
+ lines: (async function* () {
60
+ if (input.task)
61
+ yield input.task;
62
+ const reader = createInterface({ input: stdin, output: process.stderr, prompt: "› " });
63
+ reader.prompt();
64
+ for await (const line of reader) {
65
+ const said = line.trim();
66
+ // A blank line is someone thinking, not an empty question.
67
+ if (said)
68
+ yield said;
69
+ reader.prompt();
70
+ }
71
+ reader.close();
72
+ })(),
73
+ };
74
+ }
75
+ /**
76
+ * The standard pair of channels for a terminal.
77
+ *
78
+ * The answer on stdout, the account of the run on stderr, so redirecting the
79
+ * first captures the answer and nothing else. Written out identically in three
80
+ * recipes before this existed, which is two more copies than a fact deserves.
81
+ */
82
+ export function terminalSink(options = {}) {
83
+ return {
84
+ write: (text) => process.stdout.write(text),
85
+ status: (line) => process.stderr.write(line),
86
+ tty: process.stderr.isTTY === true,
87
+ ...(options.detail ? { detail: options.detail } : {}),
88
+ };
89
+ }
90
+ //# sourceMappingURL=terminal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal.js","sourceRoot":"","sources":["../src/terminal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAShD,MAAM,UAAU,SAAS,CAAC,KAMzB;IACC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC;IAC3C,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC;IAExD,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO;YACL,WAAW,EAAE,KAAK;YAClB,KAAK,EAAE,CAAC,KAAK,SAAS,CAAC;gBACrB,uEAAuE;gBACvE,wBAAwB;gBACxB,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;oBAAC,MAAM,KAAK,CAAC,IAAI,CAAC;oBAAC,OAAO;gBAAC,CAAC;gBAC7C,MAAM,KAAK,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,EAAE;oBAClD,IAAI,IAAI,GAAG,EAAE,CAAC;oBACd,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;oBAC1B,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;oBAChD,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;gBACvC,CAAC,CAAC,CAAC;gBACH,IAAI,KAAK,CAAC,IAAI,EAAE;oBAAE,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;YACvC,CAAC,CAAC,EAAE;SACL,CAAC;IACJ,CAAC;IAED,OAAO;QACL,WAAW,EAAE,IAAI;QACjB,KAAK,EAAE,CAAC,KAAK,SAAS,CAAC;YACrB,IAAI,KAAK,CAAC,IAAI;gBAAE,MAAM,KAAK,CAAC,IAAI,CAAC;YACjC,MAAM,MAAM,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YACvF,MAAM,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;gBAChC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;gBACzB,2DAA2D;gBAC3D,IAAI,IAAI;oBAAE,MAAM,IAAI,CAAC;gBACrB,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,CAAC;YACD,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,CAAC,CAAC,EAAE;KACL,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,UAAuC,EAAE;IACpE,OAAO;QACL,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;QAC3C,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;QAC5C,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI;QAClC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtD,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Where a person's turns come from.\n *\n * The inbound half of the one channel this package ships, and `render` is the\n * outbound half. A channel does three things — decide which session a person is\n * talking to, deliver a message exactly once, and put the result in a medium's\n * own shape. For a terminal the first is \"the one session\" and the second is a\n * pipe that cannot fail, so the two halves collapse to a keyboard and a screen,\n * and both are the same for every application. A mail bridge keeps all three\n * and is yours; this is the degenerate case, which is why it can be here.\n *\n * It is the one file in `src` that touches `process`, and it is confined to\n * that: `render` takes a sink, this takes a stream, and neither reaches for a\n * terminal it was not handed.\n *\n * **Multi-turn is the default, and it is not a flag.** A conversation is what\n * this recipe is for, and one-shot is the special case — the one where nobody\n * is there. So the fact that decides it is whether stdin is a terminal, not\n * something an operator has to remember:\n *\n * - A person at a terminal gets a prompt, and any task on argv is simply\n * their first turn.\n * - A pipe or a redirect gets one shot and an exit code, so\n * `recipe native-agent \"…\" > answer.txt` and `echo … | recipe` both behave\n * the way every other command-line program does.\n *\n * There is no flag either way. `--once` existed to give a person at a terminal\n * the script behaviour, and `echo \"…\" | recipe` already does that — a second\n * way to say a thing the shell says better. Prompting into a pipe has no flag\n * for the opposite reason: it would wait for a person who is not there.\n */\nimport { createInterface } from \"node:readline\";\nimport type { Sink } from \"./render.js\";\n\nexport interface TurnSource {\n /** Whether a person is waiting, which decides prompting and how a failure ends. */\n interactive: boolean;\n lines: AsyncIterable<string>;\n}\n\nexport function turnsFrom(input: {\n /** The task on argv, if there was one. */\n task: string;\n /** Injected so a test never touches a terminal. */\n stdin?: NodeJS.ReadStream;\n isTTY?: boolean;\n}): TurnSource {\n const stdin = input.stdin ?? process.stdin;\n const interactive = input.isTTY ?? stdin.isTTY === true;\n\n if (!interactive) {\n return {\n interactive: false,\n lines: (async function* () {\n // A task on argv wins; otherwise the whole of stdin is the task, which\n // is what a pipe means.\n if (input.task) { yield input.task; return; }\n const piped = await new Promise<string>((resolve) => {\n let read = \"\";\n stdin.setEncoding(\"utf8\");\n stdin.on(\"data\", (chunk) => { read += chunk; });\n stdin.on(\"end\", () => resolve(read));\n });\n if (piped.trim()) yield piped.trim();\n })(),\n };\n }\n\n return {\n interactive: true,\n lines: (async function* () {\n if (input.task) yield input.task;\n const reader = createInterface({ input: stdin, output: process.stderr, prompt: \"› \" });\n reader.prompt();\n for await (const line of reader) {\n const said = line.trim();\n // A blank line is someone thinking, not an empty question.\n if (said) yield said;\n reader.prompt();\n }\n reader.close();\n })(),\n };\n}\n\n/**\n * The standard pair of channels for a terminal.\n *\n * The answer on stdout, the account of the run on stderr, so redirecting the\n * first captures the answer and nothing else. Written out identically in three\n * recipes before this existed, which is two more copies than a fact deserves.\n */\nexport function terminalSink(options: { detail?: Sink[\"detail\"] } = {}): Sink {\n return {\n write: (text) => process.stdout.write(text),\n status: (line) => process.stderr.write(line),\n tty: process.stderr.isTTY === true,\n ...(options.detail ? { detail: options.detail } : {}),\n };\n}\n"]}
@@ -62,7 +62,9 @@ export function createExecutor(input) {
62
62
  const context = {
63
63
  sessionId: input.sessionId, runId: input.runId, callId: call.callId,
64
64
  signal: signal ?? new AbortController().signal,
65
- enqueue: input.enqueue,
65
+ // Stamped here, and deliberately not taken from the caller: this is
66
+ // the one place that knows the sender without being told.
67
+ enqueue: (delivery) => input.enqueue({ ...delivery, from: { kind: "session", id: input.sessionId } }),
66
68
  report: (data) => input.report(call.callId, data),
67
69
  };
68
70
  const annotations = tool.spec.annotations;
@@ -1 +1 @@
1
- {"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/tools/execute.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Decide, Tool, ToolContext, ToolExecutor } from \"./tool.js\";\nimport type { Delivery } from \"../session/entry.js\";\nimport type { ToolCall, ToolResult } from \"../session/entry.js\";\nimport type { ToolSpec } from \"./tool.js\";\n\nimport type { JsonValue } from \"../json.js\";\n\n/**\n * The one place a tool call becomes an effect: parse, validate, decide, run,\n * record. Failures are model-visible results, never throws — a model that gets\n * an exception learns nothing, while one that gets \"invalid arguments: x must\n * be a number\" fixes its next call.\n *\n * A denial is a result like any other, so the model reads why and can act on\n * it. Nothing here parks a batch waiting on a human: see `Decide`.\n */\nexport function createExecutor(input: {\n tools: readonly Tool[];\n decide?: Decide;\n sessionId: string;\n runId: string;\n enqueue(delivery: Delivery): void;\n report(callId: string, data: JsonValue): void;\n /** Adjacent read-only calls run together; this caps how many at once. */\n maxConcurrency?: number;\n}): ToolExecutor {\n const byName = new Map<string, Tool>();\n for (const tool of input.tools) {\n if (byName.has(tool.spec.name)) throw new Error(`duplicate tool '${tool.spec.name}'`);\n byName.set(tool.spec.name, tool);\n }\n const limit = input.maxConcurrency ?? 8;\n const list = (): readonly ToolSpec[] => [...byName.values()].map((tool) => tool.spec);\n const failed = (content: string): ToolResult => ({ content, isError: true });\n\n return {\n list,\n async execute({ calls, signal }) {\n interface Planned {\n callId: string;\n concurrent: boolean;\n run(): Promise<ToolResult>;\n }\n const planned: Planned[] = [];\n const seen = new Set<string>();\n\n for (const call of calls) {\n const settled = (result: ToolResult): Planned =>\n ({ callId: call.callId, concurrent: true, run: async () => result });\n\n if (seen.has(call.callId)) { planned.push(settled(failed(`call id '${call.callId}' was reused`))); continue; }\n seen.add(call.callId);\n\n const tool = byName.get(call.name);\n if (!tool) { planned.push(settled(failed(`Unknown tool '${call.name}'`))); continue; }\n\n let raw: unknown;\n try { raw = JSON.parse(call.arguments); }\n catch { planned.push(settled(failed(\"Tool arguments are not valid JSON\"))); continue; }\n\n const prepared = tool.prepare(raw);\n if (!prepared.ok) { planned.push(settled(prepared.error)); continue; }\n\n // Decisions always see parsed, schema-valid arguments — never the raw\n // string, and never before validation.\n const decision = input.decide\n ? await input.decide({\n tool: tool.spec, input: prepared.value.input,\n sessionId: input.sessionId, runId: input.runId, callId: call.callId,\n })\n : { action: \"execute\" as const };\n\n if (decision.action === \"reject\") {\n planned.push(settled(failed(`Tool use denied: ${decision.message}`)));\n continue;\n }\n\n const context: ToolContext = {\n sessionId: input.sessionId, runId: input.runId, callId: call.callId,\n signal: signal ?? new AbortController().signal,\n enqueue: input.enqueue,\n report: (data) => input.report(call.callId, data),\n };\n const annotations = tool.spec.annotations;\n planned.push({\n callId: call.callId,\n concurrent: annotations?.readOnly === true && annotations.sequential !== true,\n run: async () => {\n try { return await prepared.value.run(context); }\n catch (error) { return failed(error instanceof Error ? error.message : String(error)); }\n },\n });\n }\n\n const results: { callId: string; result: ToolResult }[] = [];\n let index = 0;\n while (index < planned.length) {\n if (signal?.aborted) {\n for (const item of planned.slice(index)) {\n results.push({ callId: item.callId, result: failed(\"Tool use cancelled\") });\n }\n break;\n }\n let end = index + 1;\n if (planned[index]!.concurrent) {\n while (end < planned.length && planned[end]!.concurrent && end - index < limit) end += 1;\n }\n const segment = planned.slice(index, end);\n const settled = await Promise.all(segment.map((item) => item.run()));\n segment.forEach((item, offset) => results.push({ callId: item.callId, result: settled[offset]! }));\n index = end;\n }\n return { results };\n },\n };\n}\n\nexport type { ToolCall };\n"]}
1
+ {"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/tools/execute.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Decide, Tool, ToolContext, ToolExecutor } from \"./tool.js\";\nimport type { Delivery } from \"../session/entry.js\";\nimport type { ToolCall, ToolResult } from \"../session/entry.js\";\nimport type { ToolSpec } from \"./tool.js\";\n\nimport type { JsonValue } from \"../json.js\";\n\n/**\n * The one place a tool call becomes an effect: parse, validate, decide, run,\n * record. Failures are model-visible results, never throws — a model that gets\n * an exception learns nothing, while one that gets \"invalid arguments: x must\n * be a number\" fixes its next call.\n *\n * A denial is a result like any other, so the model reads why and can act on\n * it. Nothing here parks a batch waiting on a human: see `Decide`.\n */\nexport function createExecutor(input: {\n tools: readonly Tool[];\n decide?: Decide;\n sessionId: string;\n runId: string;\n enqueue(delivery: Delivery): void;\n report(callId: string, data: JsonValue): void;\n /** Adjacent read-only calls run together; this caps how many at once. */\n maxConcurrency?: number;\n}): ToolExecutor {\n const byName = new Map<string, Tool>();\n for (const tool of input.tools) {\n if (byName.has(tool.spec.name)) throw new Error(`duplicate tool '${tool.spec.name}'`);\n byName.set(tool.spec.name, tool);\n }\n const limit = input.maxConcurrency ?? 8;\n const list = (): readonly ToolSpec[] => [...byName.values()].map((tool) => tool.spec);\n const failed = (content: string): ToolResult => ({ content, isError: true });\n\n return {\n list,\n async execute({ calls, signal }) {\n interface Planned {\n callId: string;\n concurrent: boolean;\n run(): Promise<ToolResult>;\n }\n const planned: Planned[] = [];\n const seen = new Set<string>();\n\n for (const call of calls) {\n const settled = (result: ToolResult): Planned =>\n ({ callId: call.callId, concurrent: true, run: async () => result });\n\n if (seen.has(call.callId)) { planned.push(settled(failed(`call id '${call.callId}' was reused`))); continue; }\n seen.add(call.callId);\n\n const tool = byName.get(call.name);\n if (!tool) { planned.push(settled(failed(`Unknown tool '${call.name}'`))); continue; }\n\n let raw: unknown;\n try { raw = JSON.parse(call.arguments); }\n catch { planned.push(settled(failed(\"Tool arguments are not valid JSON\"))); continue; }\n\n const prepared = tool.prepare(raw);\n if (!prepared.ok) { planned.push(settled(prepared.error)); continue; }\n\n // Decisions always see parsed, schema-valid arguments — never the raw\n // string, and never before validation.\n const decision = input.decide\n ? await input.decide({\n tool: tool.spec, input: prepared.value.input,\n sessionId: input.sessionId, runId: input.runId, callId: call.callId,\n })\n : { action: \"execute\" as const };\n\n if (decision.action === \"reject\") {\n planned.push(settled(failed(`Tool use denied: ${decision.message}`)));\n continue;\n }\n\n const context: ToolContext = {\n sessionId: input.sessionId, runId: input.runId, callId: call.callId,\n signal: signal ?? new AbortController().signal,\n // Stamped here, and deliberately not taken from the caller: this is\n // the one place that knows the sender without being told.\n enqueue: (delivery) => input.enqueue({ ...delivery, from: { kind: \"session\", id: input.sessionId } }),\n report: (data) => input.report(call.callId, data),\n };\n const annotations = tool.spec.annotations;\n planned.push({\n callId: call.callId,\n concurrent: annotations?.readOnly === true && annotations.sequential !== true,\n run: async () => {\n try { return await prepared.value.run(context); }\n catch (error) { return failed(error instanceof Error ? error.message : String(error)); }\n },\n });\n }\n\n const results: { callId: string; result: ToolResult }[] = [];\n let index = 0;\n while (index < planned.length) {\n if (signal?.aborted) {\n for (const item of planned.slice(index)) {\n results.push({ callId: item.callId, result: failed(\"Tool use cancelled\") });\n }\n break;\n }\n let end = index + 1;\n if (planned[index]!.concurrent) {\n while (end < planned.length && planned[end]!.concurrent && end - index < limit) end += 1;\n }\n const segment = planned.slice(index, end);\n const settled = await Promise.all(segment.map((item) => item.run()));\n segment.forEach((item, offset) => results.push({ callId: item.callId, result: settled[offset]! }));\n index = end;\n }\n return { results };\n },\n };\n}\n\nexport type { ToolCall };\n"]}
@@ -24,8 +24,15 @@ export interface ToolContext {
24
24
  * This is the whole of agent-to-agent messaging: spawning a child, replying
25
25
  * to a parent and messaging a peer are the same call, and either the sending
26
26
  * turn commits with the delivery or neither happens.
27
+ *
28
+ * `from` is not yours to state. The executor stamps this session, because a
29
+ * tool that supplied its own sender could omit it — losing the provenance a
30
+ * recipient reads to tell a peer's message from its user's — or claim to be
31
+ * a session it is not. It was a value two callers each rebuilt from
32
+ * `sessionId`, which is a second field for a fact another field already
33
+ * determines.
27
34
  */
28
- enqueue(delivery: Delivery): void;
35
+ enqueue(delivery: Omit<Delivery, "from">): void;
29
36
  /** Progress for a live viewer. Never recovery state. */
30
37
  report(data: JsonValue): void;
31
38
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tool.js","sourceRoot":"","sources":["../../src/tools/tool.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,GAAG,EAAE,EAAE,EAAe,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA0ExB;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAA4B,KAMrD;IACC,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,CAAc,CAAC;IAC7D,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC;YAClB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,UAAU;YACV,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACjE,CAAC;QACF,OAAO,CAAC,GAAY;YAClB,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YAC3C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,OAAO,GAAG,CAAC;oBACT,OAAO,EAAE,yBAAyB,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;oBAChF,OAAO,EAAE,IAAI;iBACd,CAAC,CAAC;YACL,CAAC;YACD,OAAO,EAAE,CAAC;gBACR,KAAK,EAAE,MAAM,CAAC,IAAiB;gBAC/B,4DAA4D;gBAC5D,mEAAmE;gBACnE,GAAG,EAAE,KAAK,EAAE,OAAoB,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAyB,EAAE,OAAO,CAAC;aAC9F,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { Content } from \"../content.js\";\n\nimport type { Delivery, ToolCall, ToolResult } from \"../session/entry.js\";\nimport type { JsonValue } from \"../json.js\";\nimport { err, ok, type Result } from \"../result.js\";\nimport { z } from \"zod\";\n\n/** What a tool tells a model about itself. Declared here because a tool owns it. */\nexport interface ToolSpec {\n name: string;\n description: string;\n /** JSON Schema. */\n parameters: JsonValue;\n annotations?: { readOnly?: boolean; sequential?: boolean };\n}\n\nexport interface ToolContext {\n sessionId: string;\n runId: string;\n callId: string;\n signal: AbortSignal;\n /**\n * Deliver input to another session, committed with this run's own entries.\n * This is the whole of agent-to-agent messaging: spawning a child, replying\n * to a parent and messaging a peer are the same call, and either the sending\n * turn commits with the delivery or neither happens.\n */\n enqueue(delivery: Delivery): void;\n /** Progress for a live viewer. Never recovery state. */\n report(data: JsonValue): void;\n}\n\nexport interface Tool {\n spec: ToolSpec;\n /** Validates raw arguments once, before any authority decision sees them. */\n prepare(raw: unknown): Result<{ input: JsonValue; run(context: ToolContext): Promise<ToolResult> }, ToolResult>;\n}\n\n/**\n * The entire permission model.\n *\n * One function over parsed arguments, supplied by the application, which\n * already owns identity, tenancy and policy. There is no grant vocabulary, no\n * principal and no second batch-level hook: anything those expressed, a closure\n * expresses better.\n *\n * Two outcomes, not three. There was a `pause`, which parked the batch and\n * committed an approval request — and nothing could ever resolve it, because\n * resuming an exact batch is only possible in a harness we own, and three of\n * the four are not. An approval that works in one harness and dead-ends in the\n * others is worse than none. A call needing permission is rejected with a\n * message saying so; the application asks whoever approves, and their answer\n * arrives as ordinary input on the session, which is a path that already works\n * everywhere. `recipes/agent-service` shows the whole round trip.\n *\n * The gate does not weaken by being a rejection: this runs on every call and\n * cannot be routed around. What an application gives up is exact-argument\n * replay, since the model re-issues rather than resuming. An application that\n * needs the stronger thing keys its approval on the call id and a hash of the\n * parsed input, so a re-issue that differs misses and is refused again.\n */\nexport type Decide = (call: {\n tool: ToolSpec;\n input: JsonValue;\n sessionId: string;\n runId: string;\n callId: string;\n}) =>\n | { action: \"execute\" }\n | { action: \"reject\"; message: string }\n | Promise<{ action: \"execute\" } | { action: \"reject\"; message: string }>;\n\n/** What a harness is handed to reach application tools. */\nexport interface ToolExecutor {\n list(): readonly ToolSpec[];\n execute(input: { calls: readonly ToolCall[]; signal?: AbortSignal }):\n Promise<{ results: readonly { callId: string; result: ToolResult }[] }>;\n}\n\n/**\n * Declares a tool from a runtime schema. The argument type of `execute` is\n * derived from that schema, so there is never a hand-maintained interface to\n * keep in step with the validation.\n */\nexport function defineTool<TSchema extends z.ZodType>(input: {\n name: string;\n description: string;\n schema: TSchema;\n annotations?: ToolSpec[\"annotations\"];\n execute(args: z.output<TSchema>, context: ToolContext): ToolResult | Promise<ToolResult>;\n}): Tool {\n const parameters = z.toJSONSchema(input.schema) as JsonValue;\n return Object.freeze({\n spec: Object.freeze({\n name: input.name,\n description: input.description,\n parameters,\n ...(input.annotations ? { annotations: input.annotations } : {}),\n }),\n prepare(raw: unknown) {\n const parsed = input.schema.safeParse(raw);\n if (!parsed.success) {\n return err({\n content: `Invalid arguments for ${input.name}: ${z.prettifyError(parsed.error)}`,\n isError: true,\n });\n }\n return ok({\n input: parsed.data as JsonValue,\n // Throws are normalized by the executor, which has to cover\n // hand-written tools anyway; catching here too would be dead code.\n run: async (context: ToolContext) => input.execute(parsed.data as z.output<TSchema>, context),\n });\n },\n });\n}\n\n/** Re-exported for the same reason the model port re-exports its own. */\nexport type { Content, ToolResult };\n"]}
1
+ {"version":3,"file":"tool.js","sourceRoot":"","sources":["../../src/tools/tool.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,GAAG,EAAE,EAAE,EAAe,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiFxB;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAA4B,KAMrD;IACC,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,CAAc,CAAC;IAC7D,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC;YAClB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,UAAU;YACV,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACjE,CAAC;QACF,OAAO,CAAC,GAAY;YAClB,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YAC3C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,OAAO,GAAG,CAAC;oBACT,OAAO,EAAE,yBAAyB,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;oBAChF,OAAO,EAAE,IAAI;iBACd,CAAC,CAAC;YACL,CAAC;YACD,OAAO,EAAE,CAAC;gBACR,KAAK,EAAE,MAAM,CAAC,IAAiB;gBAC/B,4DAA4D;gBAC5D,mEAAmE;gBACnE,GAAG,EAAE,KAAK,EAAE,OAAoB,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAyB,EAAE,OAAO,CAAC;aAC9F,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { Content } from \"../content.js\";\n\nimport type { Delivery, ToolCall, ToolResult } from \"../session/entry.js\";\nimport type { JsonValue } from \"../json.js\";\nimport { err, ok, type Result } from \"../result.js\";\nimport { z } from \"zod\";\n\n/** What a tool tells a model about itself. Declared here because a tool owns it. */\nexport interface ToolSpec {\n name: string;\n description: string;\n /** JSON Schema. */\n parameters: JsonValue;\n annotations?: { readOnly?: boolean; sequential?: boolean };\n}\n\nexport interface ToolContext {\n sessionId: string;\n runId: string;\n callId: string;\n signal: AbortSignal;\n /**\n * Deliver input to another session, committed with this run's own entries.\n * This is the whole of agent-to-agent messaging: spawning a child, replying\n * to a parent and messaging a peer are the same call, and either the sending\n * turn commits with the delivery or neither happens.\n *\n * `from` is not yours to state. The executor stamps this session, because a\n * tool that supplied its own sender could omit it — losing the provenance a\n * recipient reads to tell a peer's message from its user's — or claim to be\n * a session it is not. It was a value two callers each rebuilt from\n * `sessionId`, which is a second field for a fact another field already\n * determines.\n */\n enqueue(delivery: Omit<Delivery, \"from\">): void;\n /** Progress for a live viewer. Never recovery state. */\n report(data: JsonValue): void;\n}\n\nexport interface Tool {\n spec: ToolSpec;\n /** Validates raw arguments once, before any authority decision sees them. */\n prepare(raw: unknown): Result<{ input: JsonValue; run(context: ToolContext): Promise<ToolResult> }, ToolResult>;\n}\n\n/**\n * The entire permission model.\n *\n * One function over parsed arguments, supplied by the application, which\n * already owns identity, tenancy and policy. There is no grant vocabulary, no\n * principal and no second batch-level hook: anything those expressed, a closure\n * expresses better.\n *\n * Two outcomes, not three. There was a `pause`, which parked the batch and\n * committed an approval request — and nothing could ever resolve it, because\n * resuming an exact batch is only possible in a harness we own, and three of\n * the four are not. An approval that works in one harness and dead-ends in the\n * others is worse than none. A call needing permission is rejected with a\n * message saying so; the application asks whoever approves, and their answer\n * arrives as ordinary input on the session, which is a path that already works\n * everywhere. `recipes/agent-service` shows the whole round trip.\n *\n * The gate does not weaken by being a rejection: this runs on every call and\n * cannot be routed around. What an application gives up is exact-argument\n * replay, since the model re-issues rather than resuming. An application that\n * needs the stronger thing keys its approval on the call id and a hash of the\n * parsed input, so a re-issue that differs misses and is refused again.\n */\nexport type Decide = (call: {\n tool: ToolSpec;\n input: JsonValue;\n sessionId: string;\n runId: string;\n callId: string;\n}) =>\n | { action: \"execute\" }\n | { action: \"reject\"; message: string }\n | Promise<{ action: \"execute\" } | { action: \"reject\"; message: string }>;\n\n/** What a harness is handed to reach application tools. */\nexport interface ToolExecutor {\n list(): readonly ToolSpec[];\n execute(input: { calls: readonly ToolCall[]; signal?: AbortSignal }):\n Promise<{ results: readonly { callId: string; result: ToolResult }[] }>;\n}\n\n/**\n * Declares a tool from a runtime schema. The argument type of `execute` is\n * derived from that schema, so there is never a hand-maintained interface to\n * keep in step with the validation.\n */\nexport function defineTool<TSchema extends z.ZodType>(input: {\n name: string;\n description: string;\n schema: TSchema;\n annotations?: ToolSpec[\"annotations\"];\n execute(args: z.output<TSchema>, context: ToolContext): ToolResult | Promise<ToolResult>;\n}): Tool {\n const parameters = z.toJSONSchema(input.schema) as JsonValue;\n return Object.freeze({\n spec: Object.freeze({\n name: input.name,\n description: input.description,\n parameters,\n ...(input.annotations ? { annotations: input.annotations } : {}),\n }),\n prepare(raw: unknown) {\n const parsed = input.schema.safeParse(raw);\n if (!parsed.success) {\n return err({\n content: `Invalid arguments for ${input.name}: ${z.prettifyError(parsed.error)}`,\n isError: true,\n });\n }\n return ok({\n input: parsed.data as JsonValue,\n // Throws are normalized by the executor, which has to cover\n // hand-written tools anyway; catching here too would be dead code.\n run: async (context: ToolContext) => input.execute(parsed.data as z.output<TSchema>, context),\n });\n },\n });\n}\n\n/** Re-exported for the same reason the model port re-exports its own. */\nexport type { Content, ToolResult };\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a-dray/aglib",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "A small TypeScript toolkit for building your own agent harness.",
6
6
  "license": "MIT",
@@ -27,6 +27,14 @@
27
27
  "types": "./dist/index.d.ts",
28
28
  "import": "./dist/index.js"
29
29
  },
30
+ "./render": {
31
+ "types": "./dist/render.d.ts",
32
+ "import": "./dist/render.js"
33
+ },
34
+ "./terminal": {
35
+ "types": "./dist/terminal.d.ts",
36
+ "import": "./dist/terminal.js"
37
+ },
30
38
  "./session": {
31
39
  "types": "./dist/session/index.d.ts",
32
40
  "import": "./dist/session/index.js"
@@ -113,6 +121,7 @@
113
121
  "recipe": "bun run build && bun scripts/recipe.ts",
114
122
  "docs": "bun scripts/docs.ts",
115
123
  "docs:check": "bun scripts/docs.ts --check",
124
+ "release": "bun scripts/release.ts",
116
125
  "node:verify": "node scripts/node-verify.mjs",
117
126
  "prepack": "bun run build"
118
127
  }