libfx 0.0.14-dev.1534.g6afa7c33dac5 → 0.0.14

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.
package/README.md CHANGED
@@ -56,7 +56,8 @@ agent.session(sessionId).prompt("Start with the schema.");
56
56
  - **One turn at a time.** A prompt sent while a turn runs waits behind it, even when it reaches a different server.
57
57
  - **Turns finish on their own.** A turn runs to the end whether or not anyone reads its events, and a turn nobody reads holds no connection open: its view starts reading only once you use its events, `readable` or `result`. Stop it with `session.cancel()`, or with the `signal` you passed to `prompt()`.
58
58
  - **Steering.** `session.steer(input)` steers the running turn the way the fx CLI does. While the model streams a reply, the steer cuts it short and the turn continues with it. While a tool runs, the steer waits for the tool. A steer sent while a turn waits to start joins that turn, and when no turn runs or waits, the steer becomes the next turn. A steer with images can't join a turn, so the running turn ends at its next safe point and the steer runs next.
59
- - **Retries are safe.** Give a prompt or a steer a `messageId`, and a repeated request with the same id runs nothing new. A repeated prompt follows the original turn, and its result has `repeated: true`. One id names one input, so a steer that reuses a prompt's id is rejected with `FX_MESSAGE_ID_IN_USE`. On a queue that keeps messages past the process, such as `vercel()`'s, the sender doesn't read the session first, so the worker drops that steer and emits a `session.error` with the same code. A steer's `user_message` event carries its id as `id` where it joined a turn.
59
+ - **Retries are safe.** Give a prompt or a steer a `messageId`, and a repeated request with the same id runs nothing new. A repeated prompt follows the original turn, and its result has `repeated: true`. One id names one input, so a steer that reuses a prompt's id is rejected with `FX_MESSAGE_ID_IN_USE`. A steer's `user_message` event carries its id as `id` where it joined a turn.
60
+ - **Inputs have a size limit.** A prompt or steer whose input, with the session's `context` and settings, comes to more than 8 MiB of JSON is rejected with `FX_INPUT_TOO_LARGE` before anything is stored. The model can't take a larger one.
60
61
 
61
62
  Each conversation can bring its own settings, which apply to the turns that session object starts:
62
63
 
@@ -126,7 +127,9 @@ createFxAgent({ durability: memory() }); // nothing outlives the process
126
127
 
127
128
  On `local()`, `vercel()` and `world()`, a session id is a Workflow run id, `wrun_` followed by letters and digits. Take it from `turn.accepted` or `await agent.newSessionId()` instead of making one up. On `memory()`, any 1 to 128 letters, digits, `.`, `_` or `-` work.
128
129
 
129
- A session stores the conversation and nothing else. Every process passes its own model, credentials, tools, MCP clients and skills when it creates the agent. If a session's saved checkpoint came from a different libfx version, tool set or model, the session still loads, and `onEvent` gets a `checkpoint.mismatch` event naming what changed.
130
+ A session stores the conversation, and with each prompt and steer the caller's `context` and the session object's settings. Every process passes its own model, credentials, tools, MCP clients and skills when it creates the agent.
131
+
132
+ libfx doesn't know who is calling. Anyone with a session's id can prompt, steer, cancel, stream or checkpoint it, so your app must check that the caller may use a session before it opens it. A queue message only names an input libfx already stored: a delivery that names one the session's log doesn't hold runs nothing, and the agent's `onEvent` gets a `session.refused` event. If a session's saved checkpoint came from a different libfx version, tool set or model, the session still loads, and `onEvent` gets a `checkpoint.mismatch` event naming what changed.
130
133
 
131
134
  ### Several agents
132
135
 
@@ -139,10 +142,11 @@ export const coding = createFxAgent({ name: "coding", model: "anthropic/claude-o
139
142
 
140
143
  A session belongs to the agent that created it, and its turns run only on an agent with that name. A prompt sent through another agent, such as `coding.session(supportSessionId).prompt()`, still runs on `support`, and the sending agent's `onEvent` gets a `session.forwarded` event. An agent created without a name is named `default`.
141
144
 
142
- - Agents with the same name are copies of one agent, such as one on each server, and take turns running its sessions.
145
+ - Agents with the same name are copies of one agent, such as one on each server, and take turns running its sessions. In one process, every agent with a name on a store must have the same configuration: creating one with different options throws `FX_AGENT_NAME_IN_USE`. Agents created without a name or a durability share the name `default` and the default store, so an app that serves several customers gives each customer's agent its own name. Create an agent once, at module scope, or close it when you're done with it.
143
146
  - Keep names stable. A session keeps the name it was created with, so renaming an agent leaves its sessions with no agent to run them.
144
147
  - A process that gets a turn for a name it has no agent for refuses it with an `FX_NO_AGENT` error naming the agent, and the queue delivers it again.
145
148
  - Agents share sessions through their durability. Give agents that share sessions the same durability, or none: agents created without one share the default.
149
+ - One route takes the deliveries for every World in the process and hands each to the agent of its name on the store it names. A World's store is its `store` option, or its name. An app that serves several customers gives each customer a store of its own, with `world()`'s `store`, and never shares one store between customers who must not reach each other's sessions.
146
150
 
147
151
  ### On Vercel
148
152
 
@@ -170,6 +174,8 @@ Then subscribe it to libfx's topic in `vercel.json`:
170
174
  }
171
175
  ```
172
176
 
177
+ The trigger makes the route private: it has no public URL, and only Vercel Queues can call it. Keep `wakeHandler()` on that route only. An app whose own World queues to a route the internet can reach must check that each request comes from its queue before it calls `wakeHandler()`. libfx runs only inputs its sessions already stored, but a forged request can still wake a session.
178
+
173
179
  That's all. When no other invocation holds the session, `prompt()` runs the turn in the request that called it, so the first model request goes out without a queue hop, and the function stays alive for the turn after the route responds. Vercel Queues keeps the prompt until after that request's time limit, and if the request dies mid-turn, that delivery continues the turn. Other turns go through Vercel Queues to this route:
174
180
 
175
181
  - When a turn gets within 30 seconds of the function's time limit, libfx stops it, and the next invocation continues it. `vercel({ reserveMs })` from `libfx/durable-vercel` changes that margin.
@@ -257,7 +263,7 @@ const agent = createFxAgent({
257
263
  | `signal` | Aborts when the turn is cancelled |
258
264
  | `executionId` | The call's id, the same if the call runs again after a crash |
259
265
  | `sessionId` | The session the call belongs to |
260
- | `context` | The JSON passed as `context` to `agent.session()` |
266
+ | `context` | A copy of the JSON passed as `context` to `agent.session()`, taken when you call it |
261
267
 
262
268
  Return a string to send it as is, or any other value to send it as JSON. A thrown error reaches the model as a failed tool result. To return images, return `{ type: "libfx.tool-result", text, images }`, with base64 image data.
263
269
 
@@ -407,6 +413,45 @@ A session stores each prompt as JSON before it runs, so a session prompt takes i
407
413
 
408
414
  `onEvent` receives diagnostics apart from the model's output: request timing and retries, and session events such as `session.deadline` when a turn stops for a function's time limit. The `FxAgentEvent` type lists them all.
409
415
 
416
+ ## Instrumentation hooks
417
+
418
+ Pass `hooks` to `createFxAgent()` or `createFxEngine()` to observe semantic execution on both native and Wasm backends. Your host owns span topology, naming, redaction, and export. libfx doesn't load a tracing library.
419
+
420
+ ```ts
421
+ import { createFxAgent, type FxHookEvent } from "libfx";
422
+
423
+ const observed: FxHookEvent[] = [];
424
+ const agent = createFxAgent({
425
+ hooks: {
426
+ captureContent: true,
427
+ onEvent(event) { observed.push(event); },
428
+ onError(error) { console.error("Instrumentation callback failed", error); },
429
+ },
430
+ });
431
+ ```
432
+
433
+ `FxHookEvent` includes turn and step boundaries, model requests and completions, provider admission and first output, tool execution and accepted results, permission evaluation, and journal restore/commit/error receipts. Events carry the session ID, logical turn ID, and a fresh execution ID for each prompt or resume. Model-call and step ordinals are scoped to that execution; tool call IDs come from the model and survive recovery. Join a tool to its issuing model through `model.end.data.toolCalls`, without matching tool names or parsing UI events.
434
+
435
+ `tool.end` reports execution; `tool.result` means the result entered the conversation, not that storage accepted it. `journal.commit` follows a successful append and includes its validated cursor and sequence range. Journal receipts are session-scoped, with null turn and execution IDs. A yielded or paused turn stays resumable; `turn.end.data.handoff` identifies an explicit host handoff. Replaying saved results doesn't emit fresh tool execution events; an authorized rerun emits `tool.start` with `rerun: true` and the original call ID.
436
+
437
+ `wait.start` and `wait.end` describe actual input requests separately from permission evaluation. Their `kind` is `approval`, `question`, `mcp_form`, or `mcp_url`; request IDs pair outcomes, including denial, cancellation, and failure. Native delegation events carry the child session and durable work ID. Child events retain that work ID, their own session/turn/execution identity, and the root and parent session IDs, so the host can join them even if delivery interleaves.
438
+
439
+ MCP events include the configured server, original remote tool name, and owning fx call ID. `mcp.request.data.requestIdKind` distinguishes a native JSON-RPC ID, an MCP-v2 engine handle, and an explicitly propagated host correlation token. Hooks don't change remote MCP requests by default: the host adapter emits `mcp.start` and `mcp.end` without adding metadata or emitting a request ID. Opt in with `createMcpAdapter(client, { propagateCorrelation: true })` to send an opaque UUID as `_meta["libfx/correlationId"]` and record it in `mcp.request`. It contains no internal execution/call IDs and doesn't request progress notifications. Give the adapter a `serverName` for a stable local identity; otherwise it uses the client's reported server name when available. Native resource/prompt operations also report their server and action.
440
+
441
+ `mcp.end.status` uses `success`, `failure`, `cancelled`, `input_required`, `unavailable`, or `unknown`. Native tool/protocol failures map to `failure`; `unavailable` means no operation result was available, and `unknown` means observation couldn't classify a returned host result.
442
+
443
+ Host tools receive `context.observation` when hooks are enabled. Wrap host-owned approvals or questions with `observation.wait(request, effect)`; the effect still owns the decision, response, cancellation, and error. For a host-created child engine, pass `hooks: context.observation.childHooks()` to retain the parent tool relationship. These helpers are optional, and neither requires a tracing library. Native review, vision, title, and compaction calls use the model event family with an explicit purpose; tool-owned model calls include `parentCallId`.
444
+
445
+ Always call the host effect when `context.observation` is absent too. Disabling hooks must not skip an approval or question; the observer doesn't make permission decisions.
446
+
447
+ JavaScript host-tool callbacks apply to the engine's own root session, not native descendant sessions. Descendant hook metadata doesn't authorize a cross-session host-tool request. A host-created child engine has its own root session and can install `childHooks()` for lineage.
448
+
449
+ Content capture defaults to off. Enabling it includes bounded messages, instructions, tool arguments/results, input prompts/answers, and schemas, which can contain sensitive application content. Each event has a shared 64 KiB text budget. For model requests, the newest messages are kept first from the last 128 messages, followed by instructions, tool descriptions, and schemas. Collections keep at most 128 entries, and tool `inputSchemaJson` fields keep at most 8 KiB each. `contentTruncated` marks omissions. JSON text fields can be prefixes after truncation; `inputSchemaJson` is complete JSON or null. Credentials, request headers, raw image bytes, and opaque provider replay state aren't fields in this protocol. Apply your own redaction before export.
450
+
451
+ Callbacks run in delivery order, but libfx doesn't await their promises. Callback throws and rejections go to `hooks.onError` and don't fail or retry agent work. A slow synchronous callback still delays delivery, so enqueue events and let the host own buffering and flushing. Hooks are best-effort observations, not a durable trace outbox or exactly-once delivery contract. Existing `onEvent` diagnostics and turn UI streams remain separate.
452
+
453
+ Auxiliary title-model calls can outlive `turn.end` and report model events with the same turn/execution IDs afterward. Their model-call IDs and `purpose: "title"` distinguish them from the finished agent loop.
454
+
410
455
  ## Without sessions
411
456
 
412
457
  `createFxEngine()` is the engine under the agent: one conversation in the memory of one process, with no queue and no session store. Use it for a single request, a subagent inside a tool call, or a conversation you store yourself.
package/chatgpt.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // ChatGPT (Codex) credentials for libfx agents: the Codex endpoints, how a
2
2
  // ChatGPT plan's credentials are checked and refreshed, and the headers that
3
- // carry them. `subscription.js` adds them at the fetch edge.
3
+ // carry them. `subscription.js` adds them at the fetch edge. TLA+ model:
4
+ // `tests/tla/sdk/ProviderAuth.tla`.
4
5
  import { normalizeCredentials } from "./subscription.js";
5
6
 
6
7
  export const chatgptIssuer = "https://auth.openai.com";