@procedures/llm 0.4.0 → 0.6.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 (67) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +114 -9
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/anthropic.d.ts +64 -5
  5. package/dist/anthropic.d.ts.map +1 -1
  6. package/dist/anthropic.js +109 -50
  7. package/dist/anthropic.js.map +1 -1
  8. package/dist/errors.d.ts +15 -8
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/errors.js +4 -1
  11. package/dist/errors.js.map +1 -1
  12. package/dist/events.d.ts +21 -0
  13. package/dist/events.d.ts.map +1 -0
  14. package/dist/events.js +218 -0
  15. package/dist/events.js.map +1 -0
  16. package/dist/index.d.ts +4 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +4 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/module.d.ts +8 -2
  21. package/dist/module.d.ts.map +1 -1
  22. package/dist/module.js +127 -12
  23. package/dist/module.js.map +1 -1
  24. package/dist/procedures/llm/complete.d.ts +9 -3
  25. package/dist/procedures/llm/complete.d.ts.map +1 -1
  26. package/dist/procedures/llm/complete.js +50 -10
  27. package/dist/procedures/llm/complete.js.map +1 -1
  28. package/dist/procedures/llm/index.d.ts +1 -0
  29. package/dist/procedures/llm/index.d.ts.map +1 -1
  30. package/dist/procedures/llm/index.js +1 -0
  31. package/dist/procedures/llm/index.js.map +1 -1
  32. package/dist/procedures/llm/stream.d.ts +14 -0
  33. package/dist/procedures/llm/stream.d.ts.map +1 -0
  34. package/dist/procedures/llm/stream.js +20 -0
  35. package/dist/procedures/llm/stream.js.map +1 -0
  36. package/dist/procedures/llm/structured.d.ts +1 -1
  37. package/dist/procedures/llm/structured.d.ts.map +1 -1
  38. package/dist/procedures/llm/structured.js +6 -4
  39. package/dist/procedures/llm/structured.js.map +1 -1
  40. package/dist/request.d.ts +15 -4
  41. package/dist/request.d.ts.map +1 -1
  42. package/dist/request.js +47 -2
  43. package/dist/request.js.map +1 -1
  44. package/dist/retry.d.ts +45 -0
  45. package/dist/retry.d.ts.map +1 -0
  46. package/dist/retry.js +106 -0
  47. package/dist/retry.js.map +1 -0
  48. package/dist/tools.d.ts +50 -0
  49. package/dist/tools.d.ts.map +1 -0
  50. package/dist/tools.js +162 -0
  51. package/dist/tools.js.map +1 -0
  52. package/dist/types.d.ts +138 -4
  53. package/dist/types.d.ts.map +1 -1
  54. package/package.json +9 -5
  55. package/src/anthropic.ts +149 -49
  56. package/src/errors.ts +17 -5
  57. package/src/events.ts +218 -0
  58. package/src/index.ts +5 -2
  59. package/src/module.ts +143 -12
  60. package/src/procedures/llm/complete.ts +53 -12
  61. package/src/procedures/llm/index.ts +1 -0
  62. package/src/procedures/llm/stream.ts +23 -0
  63. package/src/procedures/llm/structured.ts +6 -4
  64. package/src/request.ts +47 -4
  65. package/src/retry.ts +110 -0
  66. package/src/tools.ts +178 -0
  67. package/src/types.ts +148 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @procedures/llm
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [f3e23e3]
8
+ - @procedures/json@0.6.0
9
+ - @procedures/client@0.6.0
10
+ - @procedures/http@0.6.0
11
+
12
+ ## 0.5.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 11f9aa9: Streaming, tool use and an automatic retry for the procedures of `@procedures/llm`:
17
+
18
+ - The new procedure `llm.stream` sends the request with `"stream": true`. It gives the server-sent events of the Messages API as stream items: `start`, `text`, `tool_use` and `stop`. The `stop` item has the stop reason, the usage and the full text. `sseEvents` of `@procedures/http` parses the stream. The procedure declares `streaming: true` and `stream: { unbounded: false }`.
19
+ - `llm.complete` and `llm.stream` take `tools` (name, description, input schema, strict) and `toolChoice`. The output gives each `tool_use` block.
20
+ - With `procedures`, `llm.complete` gives the model the named procedures as tools. It calls each one through `ctx.client`, sends the `tool_result` back, and asks again until the model stops. `maxRounds` (default 8) ends the loop. The output has `toolCalls`, `rounds` and `roundLimit`.
21
+ - `llm.complete` is data-driven (`markDataDriven`), so the host examines each nested call with its capability and its expose rule. A refused procedure gives `FORBIDDEN` or `NOT_EXPOSED` before a request goes out.
22
+ - A request that fails with `retryable: true` (408, 409, 429, 529 and the other 5xx statuses, a connection error, an early error event of a stream) gets new tries. The wait is the `retry-after-ms` or `retry-after` header, or `backoffDelay` of `@mark1russell7/service/backoff`. The deadline stops the retry, and a stream tries again only before its first item.
23
+ - The input `retries` (0 to 10, default 2) of `llm.complete` and `llm.stream` sets the new tries. `retries: 0` turns them off. `llm.structured` keeps `retries` for the answers that are not valid, and gets `requestRetries`.
24
+ - `createLlmModule()` takes `retries` and `backoff`. `LlmError` has `retryAfterMs` and the new code `BAD_TOOL`.
25
+ - `timeoutMs` limits each request to the API.
26
+
27
+ ### Patch Changes
28
+
29
+ - 0f75644: The dependencies go to `@mark1russell7/algebra` `^0.7.0` and `@mark1russell7/service` `^0.4.1`. A host that uses algebra 0.7.0 gets one copy of it. Algebra 0.7.0 only adds to 0.6.0.
30
+ - Updated dependencies [0f75644]
31
+ - Updated dependencies [09e5566]
32
+ - Updated dependencies [ab77f75]
33
+ - Updated dependencies [a47bee6]
34
+ - Updated dependencies [bc74091]
35
+ - @procedures/client@0.5.0
36
+ - @procedures/http@0.5.0
37
+ - @procedures/json@0.5.0
38
+
3
39
  ## 0.4.0
4
40
 
5
41
  ### Patch Changes
package/README.md CHANGED
@@ -6,20 +6,21 @@
6
6
  npm install @procedures/llm
7
7
  ```
8
8
 
9
- A language model as procedures: a completion, and a structured answer that a JSON Schema accepts. The procedures send each request to the Anthropic Messages API.
9
+ A language model as procedures: a completion, a stream of the answer, and a structured answer that a JSON Schema accepts. The procedures send each request to the Anthropic Messages API.
10
10
 
11
11
  ## The procedures
12
12
 
13
13
  | Procedure | What it does | Effects |
14
14
  |---|---|---|
15
- | `llm.complete` | It sends a prompt and a system text, and gives the text, the model, the stop reason and the usage. | reads and writes `network` |
15
+ | `llm.complete` | It sends a prompt and a system text, and gives the text, the model, the stop reason and the usage. The model can use tools and procedures. | reads and writes `network` |
16
+ | `llm.stream` | It sends the same request with `"stream": true`, and gives the events of the answer as stream items. | reads and writes `network` |
16
17
  | `llm.structured` | It sends a prompt and a JSON Schema, and gives a value that the schema accepts. | reads and writes `network` |
17
18
 
18
19
  - Each procedure declares `reads: ["network"]` and `writes: ["network"]`, with `destructive: false` and `idempotent: false`. A call sends data out of the host and costs money.
19
20
  - The input has `prompt`, `system`, `model`, `maxTokens`, `temperature`, `effort`, `timeoutMs` and `fallback`.
20
21
  - The default model is `claude-sonnet-5-5`. The default of `maxTokens` is 16000, and the limit counts the tokens of the thinking too.
21
22
  - Some models accept only the default temperature. For such a model, the API refuses another value with an error.
22
- - The text of `llm.complete` is the text blocks of the answer, joined. The thinking blocks are not in it.
23
+ - The text of `llm.complete` is the text blocks of the last answer, joined. The thinking blocks are not in it.
23
24
 
24
25
  ```ts
25
26
  import "@procedures/llm/register";
@@ -40,6 +41,106 @@ The key comes from an environment variable. The default variable is `ANTHROPIC_A
40
41
  - An error message does not contain the key.
41
42
  - The procedures send the request with `fetch`, not through `http.request`. The input of a nested call is visible to the interceptors and to the traces of the host, and the key is in a header.
42
43
 
44
+ ## Streaming
45
+
46
+ `llm.stream` sends the request with `"stream": true` and reads the server-sent events of the answer. `sseEvents` of `@procedures/http` parses the event stream. Each item has a `type`:
47
+
48
+ | Item | Event of the API | Fields |
49
+ |---|---|---|
50
+ | `start` | `message_start` | `id`, `model`, and the `usage` at the start: the tokens of the input |
51
+ | `text` | `content_block_delta` with `text_delta` | `index` of the content block, and the new `text` |
52
+ | `tool_use` | `content_block_stop` of a `tool_use` block | `index`, `id`, `name`, and the parsed `input` |
53
+ | `stop` | `message_stop` | `model`, `stopReason`, the full `usage`, the full `text` and the `toolUses` |
54
+
55
+ ```ts
56
+ for await (const item of client.stream(["llm", "stream"], { prompt: "Write a haiku." })) {
57
+ if (item.type === "text") process.stdout.write(item.text);
58
+ }
59
+ ```
60
+
61
+ - The stream has the key rule, the injectable `fetch`, the time limit, the deadline and the abort of the other procedures.
62
+ - The usage of a `message_delta` event is cumulative. The `stop` item has the last value.
63
+ - `client.call` of a stream gives its last item. For `llm.stream`, that is the `stop` item, with the full text.
64
+ - The procedure ignores the `ping` events, the thinking deltas and the event types that it does not know.
65
+ - A `tool_use` item gives `input: null` and the text in `partialJson` when the JSON of the input does not parse. An answer that stops at `maxTokens` can do this.
66
+ - After a server-side fallback, the `stop` item gives the fallback model.
67
+ - An `error` event gives `API_ERROR`. An overload, a rate limit and a server error are retryable.
68
+ - A stream that ends before `message_stop` gives `CONNECTION`, with `retryable: true`.
69
+ - A refusal at the end of the stream gives `REFUSAL`, as in `llm.complete`.
70
+ - The procedure declares `streaming: true` and `stream: { unbounded: false }`. The answer ends: `maxTokens` and `timeoutMs` limit it.
71
+
72
+ ## Tools
73
+
74
+ `llm.complete` and `llm.stream` take `tools` and `toolChoice`.
75
+
76
+ - Each tool has `name`, `description`, `inputSchema` and `strict`. The name has 1 to 128 letters, digits, `_` and `-`. The API gets the schema as `input_schema`.
77
+ - `toolChoice` is `{ type: "auto" }` (the default), `{ type: "any" }`, `{ type: "tool", name }` or `{ type: "none" }`. The API gets it as `tool_choice`.
78
+ - `disableParallelToolUse: true` on `auto`, `any` or `tool` lets the model use at most one tool in an answer.
79
+ - The output of `llm.complete` gives each `tool_use` block of the answer in `toolUses`: `id`, `name` and `input`. The caller starts these tools.
80
+ - Claude Sonnet 5.5, Claude Opus 5.5 and Claude Fable 5.1 refuse `any` and `tool` with a 400 error. For these models, use `auto`, a clear prompt and `strict: true`.
81
+ - Two tools with the same name, a `toolChoice` without tools, and a `toolChoice` with an unknown name give `BAD_TOOL`, before a request goes out.
82
+
83
+ ## Procedures as tools
84
+
85
+ With `procedures`, `llm.complete` starts the tools itself. The input names the keys of the procedures, for example `weather.current`.
86
+
87
+ ```ts
88
+ const { text, toolCalls, rounds } = await client.call(["llm", "complete"], {
89
+ prompt: "What is the weather in Paris?",
90
+ procedures: ["weather.current"],
91
+ maxRounds: 4,
92
+ });
93
+ ```
94
+
95
+ 1. Each procedure becomes a tool. The name is the key with `_` in place of each `.`, for example `weather_current`.
96
+ 2. The description of the procedure and the JSON Schema of its input go to the API. The input of the procedure must be an object.
97
+ 3. When the model asks for a tool, `llm.complete` starts the procedure through `ctx.client` with the input of the model.
98
+ 4. The output goes back as a `tool_result`, and the next request has the answer and the results.
99
+ 5. The loop continues until the model stops, or until `maxRounds` requests went out. The default of `maxRounds` is 8, and the maximum is 64.
100
+
101
+ The output has these fields:
102
+
103
+ | Field | Meaning |
104
+ |---|---|
105
+ | `toolCalls` | Each procedure call: `id`, `name`, `procedure`, `input`, `isError`, and `output` or `error` |
106
+ | `rounds` | The number of requests to the model |
107
+ | `roundLimit` | True when `maxRounds` stopped the loop. Then `stopReason` is `tool_use`, and `toolUses` has the blocks that no procedure got. |
108
+ | `usage` | The tokens of all the requests |
109
+
110
+ - The procedures run one at a time, in the sequence of the answer. All the results of one answer go back in one message.
111
+ - An error of a procedure goes to the model as a `tool_result` with `is_error: true`. An input that the schema refuses gives `VALIDATION_ERROR` there. The model can try again.
112
+ - A forced `toolChoice` (`any` or `tool`) applies to the first request only. The next requests use `auto`, so the model can stop.
113
+ - When an answer asks for a tool of `tools` too, no procedure runs. The output gives all the `tool_use` blocks.
114
+ - A `tool_result` has at most 200000 characters of the output. A longer output gets a note with its length.
115
+ - A procedure without an end (`stream: { unbounded: true }`) gives `BAD_TOOL`.
116
+
117
+ ### The admission of the host
118
+
119
+ `llm.complete` is a data-driven procedure (`markDataDriven` of `@procedures/client`), because its input chooses its callees. Thus the host examines each nested call, as it examines the calls of `client.chain`.
120
+
121
+ - The capability of the call applies to each procedure tool. A host that permits only `writes: ["network"]` permits only read-only tools.
122
+ - The expose rule of the host applies too. A host with `expose` lets the model call only the procedures that it exposes.
123
+ - Before the first request, `llm.complete` examines each named procedure with the same rules. A refused procedure gives `FORBIDDEN` or `NOT_EXPOSED`, and no request goes out.
124
+ - A procedure that is not in the registry gives `NOT_FOUND`. A refused path tells nothing about the procedure.
125
+ - When the host refuses a nested call later (an interceptor can narrow the capability), the call stops with that error.
126
+ - The abort of the caller and the deadline also stop the call during a tool.
127
+ - The procedure is not `runs-refs`: the refs of its input still run before the call. It declares no `calls`, because its callees come from the input.
128
+
129
+ ## Automatic retry
130
+
131
+ A request that fails with `retryable: true` gets a new try.
132
+
133
+ - The retryable errors are the statuses 408, 409, 429 and 5xx (529 too), a connection error, and an early error event of a stream.
134
+ - The header `x-should-retry` of the API overrides the rule of the status.
135
+ - The input `retries` of `llm.complete` and `llm.stream` sets the number of new tries, from 0 to 10. The default is 2, the default of the Anthropic SDKs. `retries: 0` turns the retry off.
136
+ - `llm.structured` has `retries` already, for the answers that are not valid. Its input `requestRetries` sets the new tries of a request.
137
+ - The wait before a try is the `retry-after-ms` or `retry-after` header. Without a header, the wait comes from `backoffDelay` of `@mark1russell7/service/backoff`.
138
+ - The default backoff starts at 500 ms, doubles each time, and stops at 8000 ms. A jitter removes up to a quarter of each wait.
139
+ - When the wait ends after the deadline of the call, or `retry-after` asks for more than 60 seconds, the call gives the error at once.
140
+ - The abort of the caller stops a wait with `ABORTED`.
141
+ - A stream tries again only before its first item. After the first item, an error goes to the caller.
142
+ - A `TIMEOUT` does not get a new try.
143
+
43
144
  ## Structured answers
44
145
 
45
146
  `llm.structured` gives a value that the schema accepts, or an error.
@@ -54,7 +155,7 @@ The key comes from an environment variable. The default variable is `ANTHROPIC_A
54
155
 
55
156
  ## Limits and time
56
157
 
57
- - Each call has `timeoutMs`. The default is 10 minutes, and the maximum is 1 hour.
158
+ - Each request to the API has `timeoutMs`. The default is 10 minutes, and the maximum is 1 hour. A stream gets the same limit for all its events.
58
159
  - The deadline of the call (`ctx.capability.deadline`) and the abort of the caller (`ctx.signal`) also stop the call. The earliest event wins.
59
160
  - A stop gives `TIMEOUT`, or `ABORTED` for an abort of the caller.
60
161
 
@@ -86,20 +187,24 @@ const registry = createRegistry(createLlmModule({ apiKeyEnv: "WORK_ANTHROPIC_KEY
86
187
  | `fallback` | The default of the `fallback` input. The default is `true`. |
87
188
  | `fetch` | The `fetch` of the requests. A test gives a fake one. |
88
189
  | `env` | The variables: a record, or a function that gives them at each call. The default is `process.env`. |
190
+ | `retries` | The default number of new tries after a retryable error, from 0 to 10. The default is 2. |
191
+ | `backoff` | The `Backoff` policy of `@mark1russell7/service/backoff`. A test gives a short one. |
89
192
 
90
193
  ## Errors
91
194
 
92
195
  | Error | Code | Cause |
93
196
  |---|---|---|
94
197
  | `LlmError` | `NO_API_KEY` | The variable of the key has no value. |
95
- | `LlmError` | `API_ERROR` | The API answered with an error status. `status` has it, and `retryable` is true for 408, 409, 429 and 5xx. |
96
- | `LlmError` | `CONNECTION` | The connection failed. `retryable` is true. |
97
- | `LlmError` | `BAD_RESPONSE` | The answer of the API is not a message. |
198
+ | `LlmError` | `API_ERROR` | The API answered with an error status, or a stream sent an `error` event. `status` has the status. `retryable` is true for 408, 409, 429 and 5xx. |
199
+ | `LlmError` | `CONNECTION` | The connection failed, or a stream ended before `message_stop`. `retryable` is true. |
200
+ | `LlmError` | `BAD_RESPONSE` | The answer of the API is not a message or an event stream. |
98
201
  | `LlmError` | `REFUSAL` | A safety classifier declined the request. |
202
+ | `LlmError` | `BAD_TOOL` | A tool definition is not valid. |
99
203
  | `LlmError` | `MAX_TOKENS`, `INVALID_OUTPUT` | `llm.structured` got no valid value. |
100
204
  | `InvocationError` | `TIMEOUT`, `ABORTED` | The time limit, the deadline or the abort of the caller stopped the call. |
205
+ | `InvocationError` | `FORBIDDEN`, `NOT_EXPOSED`, `NOT_FOUND` | The host refused a procedure tool. |
101
206
 
102
- The procedures do not try a failed request again. The caller decides, with `retryable`.
207
+ `retryAfterMs` of an `LlmError` gives the wait that the API asked for.
103
208
 
104
209
  ## MCP
105
210
 
@@ -107,4 +212,4 @@ These procedures are not in a bundle of MCP tools. A call costs money and sends
107
212
 
108
213
  ## Tests
109
214
 
110
- `pnpm --filter @procedures/llm test` examines the procedures with a fake `fetch`. No test sends a request to the network.
215
+ `pnpm --filter @procedures/llm test` examines the procedures with a fake `fetch`. The fake gives recorded JSON answers and recorded event streams. No test sends a request to the network.