@north-light/sdk 0.3.576 → 0.3.578
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 +1 -1
- package/dist/resources/run-reply.d.ts +1 -1
- package/dist/resources/run-reply.js +9 -3
- package/dist/resources/runs.d.ts +5 -3
- package/dist/resources/runs.js +6 -5
- package/dist/skill/northlight/SKILL.md +2 -2
- package/dist/testing/cloud.js +9 -2
- package/dist/testing/run.d.ts +5 -1
- package/dist/testing/run.js +22 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ console.log(outcome.kind);
|
|
|
116
116
|
|
|
117
117
|
## Follow the reply to a message
|
|
118
118
|
|
|
119
|
-
On a run, `runs.reply(runId, sent)` follows the turn that answers one `runs.message` and ends when that turn completes. `turn.completed.error` reports a refused (`content_refused`)
|
|
119
|
+
On a run, `runs.reply(runId, sent)` follows the turn that answers one `runs.message` and ends when that turn completes. `turn.completed.error` reports a refused (`content_refused`), failed (`model_error`) or rate-limited (`rate_limited`, with `retry_at`) model call.
|
|
120
120
|
|
|
121
121
|
```ts
|
|
122
122
|
const sent = await client.runs.message(runId, 'Hello');
|
|
@@ -61,7 +61,7 @@ export interface RunReplyCollectOptions {
|
|
|
61
61
|
/**
|
|
62
62
|
* The reply to one message: the root's turn that delivers it. Consume it once:
|
|
63
63
|
* iterate it, or call `collect()` (text plus how it ended, with an optional
|
|
64
|
-
* `onDelta` while it streams) or `text()`. It ends after `turn.completed` (or `run.settled`) and
|
|
64
|
+
* `onDelta` while it streams) or `text()`. If a later message joins the same turn, text after that message belongs to its reply; this reply still waits for the turn to complete. Messages delivered together share the reply. It ends after `turn.completed` (or `run.settled`) and
|
|
65
65
|
* closes its event stream; aborting the `signal` rejects with `APIUserAbortError`.
|
|
66
66
|
*/
|
|
67
67
|
export declare class RunReply implements AsyncIterable<RunReplyEvent> {
|
|
@@ -2,7 +2,7 @@ import { APIError } from '../errors.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* The reply to one message: the root's turn that delivers it. Consume it once:
|
|
4
4
|
* iterate it, or call `collect()` (text plus how it ended, with an optional
|
|
5
|
-
* `onDelta` while it streams) or `text()`. It ends after `turn.completed` (or `run.settled`) and
|
|
5
|
+
* `onDelta` while it streams) or `text()`. If a later message joins the same turn, text after that message belongs to its reply; this reply still waits for the turn to complete. Messages delivered together share the reply. It ends after `turn.completed` (or `run.settled`) and
|
|
6
6
|
* closes its event stream; aborting the `signal` rejects with `APIUserAbortError`.
|
|
7
7
|
*/
|
|
8
8
|
export class RunReply {
|
|
@@ -19,6 +19,8 @@ export class RunReply {
|
|
|
19
19
|
this.consumed = true;
|
|
20
20
|
let stream = this.open(this.sent.sequence_number);
|
|
21
21
|
let inTurn = false;
|
|
22
|
+
let textEnded = false;
|
|
23
|
+
let startingIds = new Set();
|
|
22
24
|
try {
|
|
23
25
|
for (;;) {
|
|
24
26
|
for await (const event of stream) {
|
|
@@ -30,11 +32,15 @@ export class RunReply {
|
|
|
30
32
|
}
|
|
31
33
|
if (!inTurn) {
|
|
32
34
|
inTurn = event.type === 'node.turn.started' && event.input.message_ids.includes(this.sent.message_id);
|
|
35
|
+
if (inTurn && event.type === 'node.turn.started')
|
|
36
|
+
startingIds = new Set(event.input.message_ids);
|
|
33
37
|
continue;
|
|
34
38
|
}
|
|
35
|
-
if (event.type === 'node.
|
|
39
|
+
if (event.type === 'node.turn.started' && event.input.message_ids.some((id) => !startingIds.has(id)))
|
|
40
|
+
textEnded = true;
|
|
41
|
+
if (!textEnded && event.type === 'node.output_text.delta')
|
|
36
42
|
yield { type: 'text.delta', delta: event.delta, sequence_number: event.sequence_number };
|
|
37
|
-
else if (event.type === 'node.output_text.done')
|
|
43
|
+
else if (!textEnded && event.type === 'node.output_text.done')
|
|
38
44
|
yield { type: 'message.done', text: event.text, stop_reason: event.stop_reason, sequence_number: event.sequence_number };
|
|
39
45
|
else if (event.type === 'node.turn.completed') {
|
|
40
46
|
yield {
|
package/dist/resources/runs.d.ts
CHANGED
|
@@ -155,8 +155,9 @@ export declare class Runs {
|
|
|
155
155
|
limit?: number;
|
|
156
156
|
}, options?: RequestOptions): AsyncGenerator<RunObject, void, undefined>;
|
|
157
157
|
/** Send a message to a run. `start_turn: true` (the default) answers it in its
|
|
158
|
-
*
|
|
159
|
-
*
|
|
158
|
+
* turn. A running run reads it at its next step unless `steer: false` opts out
|
|
159
|
+
* and waits for the current turn to end. A message sent before the first turn
|
|
160
|
+
* has started queues behind that turn,
|
|
160
161
|
* because a run's `runs.start` prompt is always its first turn's input.
|
|
161
162
|
* `start_turn: false` stores it without starting a turn; it is delivered as
|
|
162
163
|
* context at the start of the next turn, ahead of that turn's input. For a
|
|
@@ -166,10 +167,11 @@ export declare class Runs {
|
|
|
166
167
|
* The send carries an `Idempotency-Key`: `idempotencyKey` when given, otherwise a UUID generated for this
|
|
167
168
|
* call and reused on every retry of it. The daemon keeps the key for 24 hours; a repeat with the same key
|
|
168
169
|
* and the same message returns the first send's result without delivering again, and the same key with a
|
|
169
|
-
* different message or `
|
|
170
|
+
* different message, `start_turn`, or effective `steer` throws `ConflictError` `idempotency_conflict`. To make a send safe
|
|
170
171
|
* across your own restarts, derive the key from your own record of the send (a batch or row id). */
|
|
171
172
|
message(runId: string, message: string, options?: RequestOptions & {
|
|
172
173
|
start_turn?: boolean;
|
|
174
|
+
steer?: boolean;
|
|
173
175
|
idempotencyKey?: string;
|
|
174
176
|
}): Promise<SentRunMessage>;
|
|
175
177
|
/** A keyed POST under one overall deadline. `budgetMs` / `deadlineAt` (fixed here, once) bound every attempt, every
|
package/dist/resources/runs.js
CHANGED
|
@@ -41,8 +41,9 @@ export class Runs {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
/** Send a message to a run. `start_turn: true` (the default) answers it in its
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
* turn. A running run reads it at its next step unless `steer: false` opts out
|
|
45
|
+
* and waits for the current turn to end. A message sent before the first turn
|
|
46
|
+
* has started queues behind that turn,
|
|
46
47
|
* because a run's `runs.start` prompt is always its first turn's input.
|
|
47
48
|
* `start_turn: false` stores it without starting a turn; it is delivered as
|
|
48
49
|
* context at the start of the next turn, ahead of that turn's input. For a
|
|
@@ -52,11 +53,11 @@ export class Runs {
|
|
|
52
53
|
* The send carries an `Idempotency-Key`: `idempotencyKey` when given, otherwise a UUID generated for this
|
|
53
54
|
* call and reused on every retry of it. The daemon keeps the key for 24 hours; a repeat with the same key
|
|
54
55
|
* and the same message returns the first send's result without delivering again, and the same key with a
|
|
55
|
-
* different message or `
|
|
56
|
+
* different message, `start_turn`, or effective `steer` throws `ConflictError` `idempotency_conflict`. To make a send safe
|
|
56
57
|
* across your own restarts, derive the key from your own record of the send (a batch or row id). */
|
|
57
58
|
async message(runId, message, options = {}) {
|
|
58
|
-
const { idempotencyKey = crypto.randomUUID(), maxRetries = 2, start_turn, ...rest } = options;
|
|
59
|
-
const body = { message, ...(start_turn === undefined ? {} : { start_turn }) };
|
|
59
|
+
const { idempotencyKey = crypto.randomUUID(), maxRetries = 2, start_turn, steer, ...rest } = options;
|
|
60
|
+
const body = { message, ...(start_turn === undefined ? {} : { start_turn }), ...(steer === undefined ? {} : { steer }) };
|
|
60
61
|
// One key per logical send, reused on every retry: the daemon answers a repeat with the first send's
|
|
61
62
|
// result, so a retry after a lost response cannot deliver the message twice.
|
|
62
63
|
return this.keyedSend(`${runPath(runId)}/messages`, body, idempotencyKey, maxRetries, rest);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: northlight
|
|
3
3
|
description: "Use when building, debugging or shipping an app that signs people in with Northlight, starts runs on their Northlight runtime, or calls Northlight providers (sms, search, integrations, context, device) with @north-light/sdk."
|
|
4
4
|
metadata:
|
|
5
|
-
sdk-version: "0.3.
|
|
5
|
+
sdk-version: "0.3.578"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Building a Northlight app
|
|
@@ -27,7 +27,7 @@ Never text a person first: Comms refuses a send to anyone who has not texted you
|
|
|
27
27
|
|
|
28
28
|
## First: check this skill matches the SDK
|
|
29
29
|
|
|
30
|
-
This skill is for SDK version `0.3.
|
|
30
|
+
This skill is for SDK version `0.3.578`. If the project has `node_modules/@north-light/sdk/package.json`, compare its `version` with that number. If they differ, run `npx @north-light/sdk skill` in the project, then read the rewritten `.agents/skills/northlight/SKILL.md` (or `.claude/skills/northlight/SKILL.md`) instead of this copy. It needs no account. A project that has no SDK yet starts with `npx @north-light/sdk@latest init <dir>`.
|
|
31
31
|
|
|
32
32
|
## Where things run
|
|
33
33
|
|
package/dist/testing/cloud.js
CHANGED
|
@@ -966,9 +966,14 @@ export class FakeCloud {
|
|
|
966
966
|
throw invalid('message is required', 'message');
|
|
967
967
|
if (params['start_turn'] !== undefined && typeof params['start_turn'] !== 'boolean')
|
|
968
968
|
throw invalid('start_turn must be a boolean', 'start_turn');
|
|
969
|
+
if (params['steer'] !== undefined && typeof params['steer'] !== 'boolean')
|
|
970
|
+
throw invalid('steer must be a boolean', 'steer');
|
|
969
971
|
const startTurn = params['start_turn'] ?? true;
|
|
972
|
+
if (!startTurn && params['steer'] === true)
|
|
973
|
+
throw invalid('steer requires start_turn', 'steer');
|
|
974
|
+
const steer = startTurn && params['steer'] !== false;
|
|
970
975
|
const key = this.idempotencyKey(rawKey);
|
|
971
|
-
const hash = sha256Hex(JSON.stringify(['run.message', run.id, text, startTurn]));
|
|
976
|
+
const hash = sha256Hex(JSON.stringify(['run.message', run.id, text, startTurn, ...(startTurn && !steer ? ['steer:false'] : [])]));
|
|
972
977
|
if (key !== null) {
|
|
973
978
|
const entry = user.idempotent(key);
|
|
974
979
|
if (entry) {
|
|
@@ -986,7 +991,9 @@ export class FakeCloud {
|
|
|
986
991
|
const response = { run_id: run.id, message_id: accepted.message_id, sequence_number: accepted.sequence_number };
|
|
987
992
|
if (key !== null)
|
|
988
993
|
user.idempotency.set(key, { hash, runId: run.id, status: 202, body: structuredClone(response), createdAt: this.clock.now() });
|
|
989
|
-
if (startTurn)
|
|
994
|
+
if (startTurn && steer && run.isTurnOpen())
|
|
995
|
+
run.steer([accepted]);
|
|
996
|
+
else if (startTurn)
|
|
990
997
|
this.respond(run, 'message', [accepted]);
|
|
991
998
|
return { status: 202, body: response };
|
|
992
999
|
}
|
package/dist/testing/run.d.ts
CHANGED
|
@@ -134,9 +134,10 @@ export declare class FakeRun {
|
|
|
134
134
|
messageIds?: string[];
|
|
135
135
|
}): void;
|
|
136
136
|
/** The same turn ending in error: `last_turn_error` set, status idle. */
|
|
137
|
-
failTurn({ code, message, messageIds }: {
|
|
137
|
+
failTurn({ code, message, retry_at, messageIds }: {
|
|
138
138
|
code: TurnErrorDTO['code'];
|
|
139
139
|
message: string;
|
|
140
|
+
retry_at?: string;
|
|
140
141
|
messageIds?: string[];
|
|
141
142
|
}): void;
|
|
142
143
|
/** Fake-only: status `running` with `activity` overridden until the next appended event. */
|
|
@@ -157,6 +158,9 @@ export declare class FakeRun {
|
|
|
157
158
|
expireEvents(upTo: number): void;
|
|
158
159
|
/** Record an accepted message; returns it. */
|
|
159
160
|
accept(text: string, startTurn: boolean, idempotencyKey: string | null): FakeRunMessage;
|
|
161
|
+
/** Deliver accepted messages into the open root turn at the next scripted step. */
|
|
162
|
+
steer(messages: readonly FakeRunMessage[]): void;
|
|
163
|
+
isTurnOpen(): boolean;
|
|
160
164
|
/** Clear a non-canceled ending so a start-turn message runs it again (`commitReopenResident`). */
|
|
161
165
|
reopen(): void;
|
|
162
166
|
/** The app answered a question. */
|
package/dist/testing/run.js
CHANGED
|
@@ -142,11 +142,14 @@ export class FakeRun {
|
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
144
|
/** The same turn ending in error: `last_turn_error` set, status idle. */
|
|
145
|
-
failTurn({ code, message, messageIds }) {
|
|
145
|
+
failTurn({ code, message, retry_at, messageIds }) {
|
|
146
|
+
if ((code === 'rate_limited') !== (retry_at !== undefined))
|
|
147
|
+
throw new Error('failTurn: retry_at is required with code rate_limited and only allowed there');
|
|
148
|
+
const error = retry_at === undefined ? { code, message } : { code, message, retry_at };
|
|
146
149
|
this.turn(messageIds, (ids) => {
|
|
147
150
|
this.append('node.output_text.done', { text: '', stop_reason: 'error', entry_id: entryId() });
|
|
148
|
-
this.turnError =
|
|
149
|
-
return { stop_reason: 'error', ids, error
|
|
151
|
+
this.turnError = error;
|
|
152
|
+
return { stop_reason: 'error', ids, error };
|
|
150
153
|
});
|
|
151
154
|
}
|
|
152
155
|
/** Fake-only: status `running` with `activity` overridden until the next appended event. */
|
|
@@ -249,6 +252,20 @@ export class FakeRun {
|
|
|
249
252
|
this.wake();
|
|
250
253
|
return message;
|
|
251
254
|
}
|
|
255
|
+
/** Deliver accepted messages into the open root turn at the next scripted step. */
|
|
256
|
+
steer(messages) {
|
|
257
|
+
if (!this.turnOpen())
|
|
258
|
+
throw new Error('no open turn to steer');
|
|
259
|
+
const started = this.log.filter((event) => event.node_id === this.id && event.type === 'node.turn.started').at(-1);
|
|
260
|
+
const ids = [...started.input.message_ids];
|
|
261
|
+
for (const message of messages) {
|
|
262
|
+
if (!ids.includes(message.message_id))
|
|
263
|
+
ids.push(message.message_id);
|
|
264
|
+
this.append('node.turn.started', { input: { text: message.text, message_ids: [...ids] }, entry_id: entryId() });
|
|
265
|
+
message.delivered = true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
isTurnOpen() { return this.turnOpen(); }
|
|
252
269
|
/** Clear a non-canceled ending so a start-turn message runs it again (`commitReopenResident`). */
|
|
253
270
|
reopen() {
|
|
254
271
|
this.outcomeValue = undefined;
|
|
@@ -414,8 +431,9 @@ export class FakeRun {
|
|
|
414
431
|
this.answered = false;
|
|
415
432
|
// The status the run reads once this turn is closed (`run_status` is written as the turn ends).
|
|
416
433
|
const status = this.messages.some((m) => m.start_turn && !m.delivered) ? 'running' : this.pendingQuestionIds.length ? 'waiting_on_user' : 'idle';
|
|
434
|
+
const steered = this.log.filter((event) => event.node_id === this.id && event.type === 'node.turn.started').at(-1);
|
|
417
435
|
this.append('node.turn.completed', {
|
|
418
|
-
message_ids: ending.ids, run_status: status, stop_reason: ending.stop_reason,
|
|
436
|
+
message_ids: [...new Set([...ending.ids, ...steered.input.message_ids])], run_status: status, stop_reason: ending.stop_reason,
|
|
419
437
|
...(ending.error ? { error: ending.error } : {}), entry_id: entryId(),
|
|
420
438
|
});
|
|
421
439
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** This SDK's release version. Informational only: compatibility is the contract level, not this number. */
|
|
2
|
-
export declare const SDK_VERSION = "0.3.
|
|
2
|
+
export declare const SDK_VERSION = "0.3.578";
|
|
3
3
|
/** The release sent in `Crouter-Minimum-Runtime-Version` on every request. It is frozen, so a runtime from before the
|
|
4
4
|
* contract level existed (which only knows this header) refuses this SDK, while every later runtime accepts it. */
|
|
5
5
|
export declare const LEGACY_FLOOR = "0.3.423";
|
package/dist/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SDK_VERSION is written by scripts/release-version.mjs on every release; do not edit by hand.
|
|
2
2
|
/** This SDK's release version. Informational only: compatibility is the contract level, not this number. */
|
|
3
|
-
export const SDK_VERSION = '0.3.
|
|
3
|
+
export const SDK_VERSION = '0.3.578';
|
|
4
4
|
// LEGACY_FLOOR is written once by `scripts/contract.mjs release`, at the first release that carries the contract level
|
|
5
5
|
// (the `0.0.0` placeholder is replaced there and never again); do not edit by hand.
|
|
6
6
|
/** The release sent in `Crouter-Minimum-Runtime-Version` on every request. It is frozen, so a runtime from before the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/sdk",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"crouterContract": "1.
|
|
3
|
+
"version": "0.3.578",
|
|
4
|
+
"crouterContract": "1.40",
|
|
5
5
|
"description": "Typed Node and browser client for running crouter agents through the crtrd /v1 API.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"prepublishOnly": "node -e \"if (process.env.npm_config_tag !== 'next') { console.error('@north-light/sdk publishes only with --tag next; scripts/promote-sdk.mjs is the only mover of latest.'); process.exit(1); }\""
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@north-light/api": "0.3.
|
|
48
|
-
"@north-light/identity": "0.3.
|
|
49
|
-
"@north-light/plugin": "0.3.
|
|
47
|
+
"@north-light/api": "0.3.578",
|
|
48
|
+
"@north-light/identity": "0.3.578",
|
|
49
|
+
"@north-light/plugin": "0.3.578",
|
|
50
50
|
"jose": "^6.2.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|