@telorun/record-stream 0.5.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.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Built to be language-agnostic and infinitely extensible.
23
23
 
24
24
  ```bash
25
25
  # Reconcile your manifest into a running backend
26
- $ telo ./examples/hello-api.yaml
26
+ $ telo ./examples/hello-api
27
27
 
28
28
  {"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8844"}
29
29
  ```
@@ -44,161 +44,7 @@ $ telo ./examples/hello-api.yaml
44
44
 
45
45
  ## Example manifest
46
46
 
47
- Here is an example Telo application that defines a simple HTTP API:
48
-
49
- ```yaml
50
- kind: Telo.Application
51
- metadata:
52
- name: feedback
53
- version: 1.0.0
54
- description: |
55
- A complete feedback collection REST API — no code, pure YAML.
56
- Persists entries to SQLite and serves them over HTTP.
57
- targets:
58
- - Migrations
59
- - Server
60
- ---
61
- kind: Telo.Import
62
- metadata:
63
- name: Http
64
- source: std/http-server@0.5.0
65
- ---
66
- kind: Telo.Import
67
- metadata:
68
- name: Sql
69
- source: std/sql@0.3.0
70
- ---
71
- # SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
72
- kind: Sql.Connection
73
- metadata:
74
- name: Db
75
- driver: sqlite
76
- file: ./tmp/feedback.db
77
- ---
78
- # Migrations: applied automatically before the server starts
79
- kind: Sql.Migrations
80
- metadata:
81
- name: Migrations
82
- connection:
83
- kind: Sql.Connection
84
- name: Db
85
- ---
86
- kind: Sql.Migration
87
- metadata:
88
- name: Migration_20260413_182154_CreateFeedback
89
- version: 20260413_182154_CreateFeedback
90
- sql: |
91
- CREATE TABLE IF NOT EXISTS feedback (
92
- id INTEGER PRIMARY KEY AUTOINCREMENT,
93
- text TEXT NOT NULL,
94
- source TEXT,
95
- score INTEGER NOT NULL DEFAULT 0,
96
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
97
- )
98
- ---
99
- kind: Http.Server
100
- metadata:
101
- name: Server
102
- baseUrl: http://localhost:8844
103
- port: 8844
104
- logger: true
105
- openapi:
106
- info:
107
- title: Feedback API
108
- version: 1.0.0
109
- mounts:
110
- - path: /v1
111
- type: Http.Api.FeedbackRoutes
112
- ---
113
- kind: Http.Api
114
- metadata:
115
- name: FeedbackRoutes
116
- routes:
117
- # POST /v1/feedback — insert a new entry, score derived from body length heuristic
118
- - request:
119
- path: /feedback
120
- method: POST
121
- schema:
122
- body:
123
- type: object
124
- properties:
125
- text:
126
- type: string
127
- minLength: 1
128
- source:
129
- type: string
130
- required: [text]
131
- handler:
132
- kind: Sql.Exec
133
- connection:
134
- kind: Sql.Connection
135
- name: Db
136
- inputs:
137
- sql: "INSERT INTO feedback (text, source, score) VALUES (?, ?, ?)"
138
- bindings:
139
- - "${{ request.body.text }}"
140
- - "${{ request.body.source }}"
141
- - "${{ size(request.body.text) }}"
142
- response:
143
- - status: 201
144
- headers:
145
- Content-Type: application/json
146
- body:
147
- ok: true
148
- message: Feedback received
149
-
150
- # GET /v1/feedback — list all entries, newest first
151
- - request:
152
- path: /feedback
153
- method: GET
154
- handler:
155
- kind: Sql.Select
156
- connection:
157
- kind: Sql.Connection
158
- name: Db
159
- from: feedback
160
- columns: [id, text, source, score, created_at]
161
- orderBy:
162
- - { column: created_at, direction: desc }
163
- response:
164
- - status: 200
165
- headers:
166
- Content-Type: application/json
167
- body: "${{ result.rows }}"
168
-
169
- # GET /v1/feedback/{id} — fetch a single entry
170
- - request:
171
- path: /feedback/{id}
172
- method: GET
173
- schema:
174
- params:
175
- type: object
176
- properties:
177
- id:
178
- type: integer
179
- required: [id]
180
- handler:
181
- kind: Sql.Select
182
- connection:
183
- kind: Sql.Connection
184
- name: Db
185
- from: feedback
186
- columns: [id, text, source, score, created_at]
187
- where:
188
- - { column: id, op: "=", value: "${{ request.params.id }}" }
189
- response:
190
- - status: 200
191
- when: "size(result.rows) > 0"
192
- headers:
193
- Content-Type: application/json
194
- body: "${{ result.rows[0] }}"
195
- - status: 404
196
- headers:
197
- Content-Type: application/json
198
- body:
199
- ok: false
200
- message: Not found
201
- ```
47
+ See [examples/](./examples/) for a list of working applications.
202
48
 
203
49
  ## Status
204
50
 
@@ -0,0 +1,47 @@
1
+ import type { ControllerContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ import { Stream } from "@telorun/sdk";
3
+ /**
4
+ * RecordStream.OnComplete — a stream passthrough that fires a side effect once the
5
+ * input has been fully consumed. Every item forwards to `output` in order as it
6
+ * arrives (the downstream consumer streams live); the items are also retained, and
7
+ * when the input completes normally the injected `handler` Invocable is called once
8
+ * with `{ records, context }` — `records` is the full list observed, `context` is the
9
+ * opaque caller data passed through the `context` input.
10
+ *
11
+ * The canonical use is persist-while-streaming: an HTTP handler streams an AI/agent
12
+ * response to the client via `output` and, at end-of-stream, `handler` writes the turn
13
+ * to a store. The primitive stays domain-neutral — it does no CEL and knows nothing of
14
+ * SQL; the projection from `records` to whatever the store needs lives in `handler`
15
+ * (typically a Run.Sequence).
16
+ *
17
+ * `handler` is NOT called if the input errors (a thrown iterator error propagates) or
18
+ * the consumer cancels early (`break` / aborted response) — completion means the input
19
+ * ran to its end. Records are buffered in memory, bounded by the input stream's length
20
+ * (same envelope as RecordStream.Tee).
21
+ */
22
+ interface OnCompleteResource {
23
+ metadata: {
24
+ name: string;
25
+ module?: string;
26
+ };
27
+ /** Live Invocable instance after Phase 5 ref injection. */
28
+ handler: {
29
+ invoke(inputs: unknown): Promise<unknown>;
30
+ };
31
+ }
32
+ interface OnCompleteInputs {
33
+ input: AsyncIterable<unknown>;
34
+ context?: unknown;
35
+ }
36
+ interface OnCompleteOutputs {
37
+ output: Stream<unknown>;
38
+ }
39
+ declare class OnComplete implements ResourceInstance<OnCompleteInputs, OnCompleteOutputs> {
40
+ private readonly resource;
41
+ constructor(resource: OnCompleteResource);
42
+ invoke(inputs: OnCompleteInputs): Promise<OnCompleteOutputs>;
43
+ snapshot(): Record<string, unknown>;
44
+ }
45
+ export declare function register(_ctx: ControllerContext): void;
46
+ export declare function create(resource: OnCompleteResource, _ctx: ResourceContext): Promise<OnComplete>;
47
+ export {};
@@ -0,0 +1,37 @@
1
+ import { InvokeError, Stream } from "@telorun/sdk";
2
+ class OnComplete {
3
+ resource;
4
+ constructor(resource) {
5
+ this.resource = resource;
6
+ }
7
+ async invoke(inputs) {
8
+ const name = this.resource.metadata.name;
9
+ const input = inputs?.input;
10
+ if (!input || typeof input[Symbol.asyncIterator] !== "function") {
11
+ throw new InvokeError("ERR_INVALID_INPUT", `RecordStream.OnComplete "${name}": 'input' must be an AsyncIterable.`);
12
+ }
13
+ const handler = this.resource.handler;
14
+ if (!handler || typeof handler.invoke !== "function") {
15
+ throw new InvokeError("ERR_INVALID_REFERENCE", `RecordStream.OnComplete "${name}": 'handler' is not a live Invocable instance — check that Phase 5 injection ran.`);
16
+ }
17
+ const context = inputs.context;
18
+ async function* passthrough() {
19
+ const records = [];
20
+ for await (const item of input) {
21
+ records.push(item);
22
+ yield item;
23
+ }
24
+ // Input ran to its end — fire the completion handler once, then close. An error
25
+ // from the handler propagates (the stream ends by throwing); it is not swallowed.
26
+ await handler.invoke({ records, context });
27
+ }
28
+ return { output: new Stream(passthrough()) };
29
+ }
30
+ snapshot() {
31
+ return {};
32
+ }
33
+ }
34
+ export function register(_ctx) { }
35
+ export async function create(resource, _ctx) {
36
+ return new OnComplete(resource);
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/record-stream",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Stream operations on structured records — generic, format-neutral transformers.",
5
5
  "keywords": [
6
6
  "telo",
@@ -34,6 +34,11 @@
34
34
  "bun": "./src/extract-text-controller.ts",
35
35
  "import": "./dist/extract-text-controller.js"
36
36
  },
37
+ "./on-complete": {
38
+ "types": "./dist/on-complete-controller.d.ts",
39
+ "bun": "./src/on-complete-controller.ts",
40
+ "import": "./dist/on-complete-controller.js"
41
+ },
37
42
  "./tee": {
38
43
  "types": "./dist/tee-controller.d.ts",
39
44
  "bun": "./src/tee-controller.ts",
@@ -46,10 +51,11 @@
46
51
  ],
47
52
  "devDependencies": {
48
53
  "@types/node": "^20.0.0",
49
- "typescript": "^5.0.0"
54
+ "typescript": "^5.0.0",
55
+ "@telorun/sdk": "0.38.0"
50
56
  },
51
57
  "peerDependencies": {
52
- "@telorun/sdk": "0.13.0"
58
+ "@telorun/sdk": "*"
53
59
  },
54
60
  "scripts": {
55
61
  "build": "tsc -p tsconfig.lib.json"
@@ -0,0 +1,85 @@
1
+ import type { ControllerContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ import { InvokeError, Stream } from "@telorun/sdk";
3
+
4
+ /**
5
+ * RecordStream.OnComplete — a stream passthrough that fires a side effect once the
6
+ * input has been fully consumed. Every item forwards to `output` in order as it
7
+ * arrives (the downstream consumer streams live); the items are also retained, and
8
+ * when the input completes normally the injected `handler` Invocable is called once
9
+ * with `{ records, context }` — `records` is the full list observed, `context` is the
10
+ * opaque caller data passed through the `context` input.
11
+ *
12
+ * The canonical use is persist-while-streaming: an HTTP handler streams an AI/agent
13
+ * response to the client via `output` and, at end-of-stream, `handler` writes the turn
14
+ * to a store. The primitive stays domain-neutral — it does no CEL and knows nothing of
15
+ * SQL; the projection from `records` to whatever the store needs lives in `handler`
16
+ * (typically a Run.Sequence).
17
+ *
18
+ * `handler` is NOT called if the input errors (a thrown iterator error propagates) or
19
+ * the consumer cancels early (`break` / aborted response) — completion means the input
20
+ * ran to its end. Records are buffered in memory, bounded by the input stream's length
21
+ * (same envelope as RecordStream.Tee).
22
+ */
23
+ interface OnCompleteResource {
24
+ metadata: { name: string; module?: string };
25
+ /** Live Invocable instance after Phase 5 ref injection. */
26
+ handler: { invoke(inputs: unknown): Promise<unknown> };
27
+ }
28
+
29
+ interface OnCompleteInputs {
30
+ input: AsyncIterable<unknown>;
31
+ context?: unknown;
32
+ }
33
+
34
+ interface OnCompleteOutputs {
35
+ output: Stream<unknown>;
36
+ }
37
+
38
+ class OnComplete implements ResourceInstance<OnCompleteInputs, OnCompleteOutputs> {
39
+ constructor(private readonly resource: OnCompleteResource) {}
40
+
41
+ async invoke(inputs: OnCompleteInputs): Promise<OnCompleteOutputs> {
42
+ const name = this.resource.metadata.name;
43
+ const input = inputs?.input;
44
+ if (!input || typeof (input as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] !== "function") {
45
+ throw new InvokeError(
46
+ "ERR_INVALID_INPUT",
47
+ `RecordStream.OnComplete "${name}": 'input' must be an AsyncIterable.`,
48
+ );
49
+ }
50
+ const handler = this.resource.handler;
51
+ if (!handler || typeof handler.invoke !== "function") {
52
+ throw new InvokeError(
53
+ "ERR_INVALID_REFERENCE",
54
+ `RecordStream.OnComplete "${name}": 'handler' is not a live Invocable instance — check that Phase 5 injection ran.`,
55
+ );
56
+ }
57
+ const context = inputs.context;
58
+
59
+ async function* passthrough(): AsyncGenerator<unknown> {
60
+ const records: unknown[] = [];
61
+ for await (const item of input) {
62
+ records.push(item);
63
+ yield item;
64
+ }
65
+ // Input ran to its end — fire the completion handler once, then close. An error
66
+ // from the handler propagates (the stream ends by throwing); it is not swallowed.
67
+ await handler.invoke({ records, context });
68
+ }
69
+
70
+ return { output: new Stream(passthrough()) };
71
+ }
72
+
73
+ snapshot(): Record<string, unknown> {
74
+ return {};
75
+ }
76
+ }
77
+
78
+ export function register(_ctx: ControllerContext): void {}
79
+
80
+ export async function create(
81
+ resource: OnCompleteResource,
82
+ _ctx: ResourceContext,
83
+ ): Promise<OnComplete> {
84
+ return new OnComplete(resource);
85
+ }