@telorun/record-stream 0.5.1 → 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 +2 -156
- package/dist/on-complete-controller.d.ts +47 -0
- package/dist/on-complete-controller.js +37 -0
- package/package.json +7 -2
- package/src/on-complete-controller.ts +85 -0
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
|
|
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
|
-
|
|
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.
|
|
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",
|
|
@@ -47,7 +52,7 @@
|
|
|
47
52
|
"devDependencies": {
|
|
48
53
|
"@types/node": "^20.0.0",
|
|
49
54
|
"typescript": "^5.0.0",
|
|
50
|
-
"@telorun/sdk": "0.
|
|
55
|
+
"@telorun/sdk": "0.38.0"
|
|
51
56
|
},
|
|
52
57
|
"peerDependencies": {
|
|
53
58
|
"@telorun/sdk": "*"
|
|
@@ -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
|
+
}
|