@north-light/sdk 0.3.515 → 0.3.519

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/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export type { AttachmentLink } from './resources/attachments.js';
14
14
  export type { Share, ShareCreated, ShareCreateParams } from './resources/shares.js';
15
15
  export type { Doc, DocEditParams, DocWriteParams } from './resources/docs.js';
16
16
  export type { MemoryDocument, MemoryGetParams, MemoryListItem, MemoryListPage, MemoryListParams } from './resources/memory.js';
17
- export type { Question, QuestionAnswered, QuestionFields, QuestionResponses, QuestionSlot, QuestionStatus } from './resources/questions.js';
17
+ export type { Question, QuestionAnswered, QuestionCanceled, QuestionFields, QuestionKind, QuestionResponses, QuestionSlot, QuestionStatus } from './resources/questions.js';
18
18
  export type { ProviderToolEntry } from './resources/providers.js';
19
19
  export { OAuth2Client, ConnectedCrouter, DEFAULT_DIRECTORY_TIMEOUT_MS, MemoryConnectionStore, profileFromClaims } from './oauth/index.js';
20
20
  export { authorizationRefusalOf, refusalOf } from './oauth/index.js';
@@ -20,6 +20,8 @@ export interface QuestionFields {
20
20
  slots: QuestionSlot[];
21
21
  }
22
22
  export type QuestionStatus = 'open' | 'answered' | 'canceled';
23
+ /** A `decision` asks the person for a response; a `notice` asks for nothing and is acknowledged with `answer(id, {})`. */
24
+ export type QuestionKind = 'decision' | 'notice';
23
25
  /** Responses keyed by slot id. */
24
26
  export type QuestionResponses = Record<string, unknown>;
25
27
  /** A question an agent in a run asked the user. */
@@ -27,10 +29,15 @@ export interface Question extends QuestionFields {
27
29
  run_id: string;
28
30
  /** The asking node. */
29
31
  node_id: string;
32
+ kind: QuestionKind;
30
33
  status: QuestionStatus;
31
34
  /** Present once answered. */
32
35
  answer?: QuestionResponses;
33
36
  }
37
+ /** `questions.cancel`'s result: the question as `questions.get` returns it, with `status: 'canceled'`. */
38
+ export type QuestionCanceled = Question & {
39
+ status: 'canceled';
40
+ };
34
41
  export interface QuestionAnswered {
35
42
  question_id: string;
36
43
  status: 'answered';
@@ -42,8 +49,12 @@ export declare class Questions {
42
49
  constructor(request: Request);
43
50
  /** Read a question of a run the caller can read; anything else is `not_found`. */
44
51
  get(questionId: string, options?: RequestOptions): Promise<Question>;
45
- /** Answer an open question of the caller's own run. An answer that does not match a slot is
52
+ /** Answer an open question of the caller's own run; a notice is acknowledged with empty `responses`. An answer that does not match a slot is
46
53
  * `invalid_request` with `param` naming the slot, and records nothing. A question already answered or
47
54
  * canceled — including the loser of a race — is 409 `run_settled` with `details.state` and `details.result`. */
48
55
  answer(questionId: string, responses: QuestionResponses, options?: RequestOptions): Promise<QuestionAnswered>;
56
+ /** Cancel an open question of the caller's own run (or the caller's own runless provider approval): the person
57
+ * dismissed it, and the asking agent is told so. A question already answered or canceled is 409 `run_settled`
58
+ * with `details.state` and `details.result`; another app's run is `other_app_run`. A notice is canceled the same way. */
59
+ cancel(questionId: string, options?: RequestOptions): Promise<QuestionCanceled>;
49
60
  }
@@ -9,12 +9,18 @@ export class Questions {
9
9
  get(questionId, options) {
10
10
  return this.request('GET', questionPath(questionId), undefined, options);
11
11
  }
12
- /** Answer an open question of the caller's own run. An answer that does not match a slot is
12
+ /** Answer an open question of the caller's own run; a notice is acknowledged with empty `responses`. An answer that does not match a slot is
13
13
  * `invalid_request` with `param` naming the slot, and records nothing. A question already answered or
14
14
  * canceled — including the loser of a race — is 409 `run_settled` with `details.state` and `details.result`. */
15
15
  answer(questionId, responses, options = {}) {
16
16
  return this.request('POST', `${questionPath(questionId)}/answer`, { responses }, { ...options, maxRetries: 0 });
17
17
  }
18
+ /** Cancel an open question of the caller's own run (or the caller's own runless provider approval): the person
19
+ * dismissed it, and the asking agent is told so. A question already answered or canceled is 409 `run_settled`
20
+ * with `details.state` and `details.result`; another app's run is `other_app_run`. A notice is canceled the same way. */
21
+ cancel(questionId, options = {}) {
22
+ return this.request('POST', `${questionPath(questionId)}/cancel`, undefined, { ...options, maxRetries: 0 });
23
+ }
18
24
  }
19
25
  function questionPath(id) {
20
26
  return `/v1/questions/${encodeURIComponent(pathSegment(id, 'question id'))}`;
@@ -55,6 +55,8 @@ export interface RunObject {
55
55
  /** Present when the root's latest completed turn ended in error; the next turn that completes without one clears it. */
56
56
  last_turn_error?: TurnErrorDTO;
57
57
  pending_question_ids: string[];
58
+ /** Open notices (questions of kind `notice`) of the run, oldest first. A notice does not make the run `waiting_on_user`. */
59
+ open_notice_ids: string[];
58
60
  usage: {
59
61
  input_tokens: number;
60
62
  output_tokens: number;
@@ -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.515"
5
+ sdk-version: "0.3.519"
6
6
  ---
7
7
 
8
8
  # Building a Northlight app
@@ -11,7 +11,7 @@ An app on Northlight signs people in with their Northlight account, then starts
11
11
 
12
12
  ## First: check this skill matches the SDK
13
13
 
14
- This skill is for SDK version `0.3.515`. 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. That command needs no account and no crtr. A project that has no SDK yet starts with `npx @north-light/sdk@latest init <dir>`.
14
+ This skill is for SDK version `0.3.519`. 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. That command needs no account and no crtr. A project that has no SDK yet starts with `npx @north-light/sdk@latest init <dir>`.
15
15
 
16
16
  ## Where things run
17
17
 
@@ -6,7 +6,7 @@ import { FakeRun } from './run.js';
6
6
  import { type FakeRefusalCode, type FakeScope, type FakeUser, type Responder } from './users.js';
7
7
  import { type FakeError } from './wire.js';
8
8
  /** A runtime operation, named as the daemon's `OPERATIONS` table names it. */
9
- export type FakeOp = 'grant.get' | 'run.start' | 'run.list' | 'run.get' | 'run.message' | 'run.events' | 'run.interrupt' | 'run.cancel' | 'run.delete' | 'run.rename' | 'question.get' | 'question.answer' | 'trace.get' | 'memory.list' | 'memory.get' | 'provider.tools' | 'provider.call' | 'profile.list';
9
+ export type FakeOp = 'grant.get' | 'run.start' | 'run.list' | 'run.get' | 'run.message' | 'run.events' | 'run.interrupt' | 'run.cancel' | 'run.delete' | 'run.rename' | 'question.get' | 'question.answer' | 'question.cancel' | 'trace.get' | 'memory.list' | 'memory.get' | 'provider.tools' | 'provider.call' | 'profile.list';
10
10
  /** A directory operation. */
11
11
  export type FakeDirectoryOp = 'directory.discovery' | 'directory.token' | 'directory.jwks';
12
12
  /** What `intercept` / `failNext` / `gate` match: a runtime operation, `'*'` (every runtime operation), or a directory operation. */
@@ -28,6 +28,7 @@ const ROUTES = [
28
28
  ['GET', /^\/v1\/runs\/([^/]+)\/trace$/, 'trace.get'],
29
29
  ['GET', /^\/v1\/questions\/([^/]+)$/, 'question.get'],
30
30
  ['POST', /^\/v1\/questions\/([^/]+)\/answer$/, 'question.answer'],
31
+ ['POST', /^\/v1\/questions\/([^/]+)\/cancel$/, 'question.cancel'],
31
32
  ['GET', /^\/v1\/memory$/, 'memory.list'],
32
33
  ['GET', /^\/v1\/memory\/document$/, 'memory.get'],
33
34
  ['GET', /^\/v1\/providers\/tools$/, 'provider.tools'],
@@ -606,7 +607,7 @@ export class FakeCloud {
606
607
  }
607
608
  const user = sub !== null ? this.users.get(sub) : undefined;
608
609
  const runId = RUN_OPS.has(op) ? id : undefined;
609
- const questionId = op === 'question.get' || op === 'question.answer' ? id : null;
610
+ const questionId = op === 'question.get' || op === 'question.answer' || op === 'question.cancel' ? id : null;
610
611
  const questionRun = questionId === null ? undefined : user?.runs.find((run) => !run.deleted && run.questions.has(questionId));
611
612
  const call = {
612
613
  userId: user ? user.id : null, op, runId: runId ?? questionRun?.id ?? null, questionId, method: request.method, path: url.pathname,
@@ -776,6 +777,14 @@ export class FakeCloud {
776
777
  run.answer(question, responses);
777
778
  return { status: 200, body: { question_id: question, status: 'answered', answer: responses } };
778
779
  }
780
+ case 'question.cancel': {
781
+ const { run, question } = this.question(user, id);
782
+ const current = run.questionObject(question);
783
+ if (current['status'] !== 'open')
784
+ throw refuse(409, 'run_settled', `question ${question} is already ${String(current['status'])}`, { details: { state: current['status'] } });
785
+ run.cancelQuestion(question);
786
+ return { status: 200, body: run.questionObject(question) };
787
+ }
779
788
  case 'memory.list': return { status: 200, body: this.memoryList(user, query) };
780
789
  case 'memory.get': {
781
790
  this.memoryStore(query);
@@ -55,9 +55,12 @@ export interface FakeQuestion {
55
55
  slots?: RunQuestionSlotDTO[];
56
56
  provider?: string;
57
57
  tool?: string;
58
+ /** `notice` asks for nothing: it does not make the run `waiting_on_user`, emits no `waiting_on_user`, and is listed in `open_notice_ids`. Default `decision`. */
59
+ kind?: 'decision' | 'notice';
58
60
  }
59
61
  interface OpenQuestion {
60
62
  question_id: string;
63
+ kind: 'decision' | 'notice';
61
64
  title: string;
62
65
  subtitle?: string;
63
66
  steps: number;
@@ -112,6 +115,8 @@ export declare class FakeRun {
112
115
  /** Messages not yet delivered to a turn (the start's `message` included). */
113
116
  get pending(): FakeRunMessage[];
114
117
  get pendingQuestionIds(): string[];
118
+ get openNoticeIds(): string[];
119
+ private openIds;
115
120
  /** The run status, derived as the daemon's `runStatus` derives it. */
116
121
  get status(): RunStatusDTO;
117
122
  /** What the root is doing, derived from the event log as `runActivity` derives it with no open streaming block. */
@@ -156,6 +161,8 @@ export declare class FakeRun {
156
161
  reopen(): void;
157
162
  /** The app answered a question. */
158
163
  answer(questionId: string, responses: Record<string, unknown>): void;
164
+ /** The app canceled a question: the person dismissed it. */
165
+ cancelQuestion(questionId: string): void;
159
166
  questionObject(questionId: string): Record<string, unknown> | undefined;
160
167
  /** Resolves when the run rests (`idle`, `waiting_on_user`, `settled`), is deleted, `seconds` pass, or `signal` aborts. */
161
168
  waitResting(seconds: number, signal?: AbortSignal | null): Promise<void>;
@@ -74,7 +74,9 @@ export class FakeRun {
74
74
  get lastTurnError() { return this.turnError; }
75
75
  /** Messages not yet delivered to a turn (the start's `message` included). */
76
76
  get pending() { return this.messages.filter((message) => !message.delivered); }
77
- get pendingQuestionIds() { return [...this.questions.values()].filter((q) => q.status === 'open').map((q) => q.question_id); }
77
+ get pendingQuestionIds() { return this.openIds('decision'); }
78
+ get openNoticeIds() { return this.openIds('notice'); }
79
+ openIds(kind) { return [...this.questions.values()].filter((q) => q.status === 'open' && q.kind === kind).map((q) => q.question_id); }
78
80
  /** The run status, derived as the daemon's `runStatus` derives it. */
79
81
  get status() {
80
82
  if (this.outcomeValue)
@@ -115,6 +117,7 @@ export class FakeRun {
115
117
  ...(this.outcomeValue && this.hasResult ? { result: this.resultValue } : {}),
116
118
  // `last_report` is the newest report body (`runObject`); a scripted ending writes no report, so it is absent.
117
119
  pending_question_ids: status === 'settled' ? [] : this.pendingQuestionIds,
120
+ open_notice_ids: status === 'settled' ? [] : this.openNoticeIds,
118
121
  ...(this.turnError ? { last_turn_error: this.turnError } : {}),
119
122
  usage: usage(), payer: 'user', scopes: this.scopes, metadata: this.metadata, created_at: this.createdAt,
120
123
  ...(this.settledAt ? { settled_at: this.settledAt } : {}),
@@ -202,9 +205,14 @@ export class FakeRun {
202
205
  ask(question) {
203
206
  this.assertLive();
204
207
  const id = `question-${this.id}-${this.questions.size + 1}`;
205
- const slots = question.slots ?? [{ id: 'answer', kind: 'text', step: 0, config: {} }];
208
+ const kind = question.kind ?? 'decision';
209
+ const slots = question.slots ?? (kind === 'notice' ? [] : [{ id: 'answer', kind: 'text', step: 0, config: {} }]);
210
+ if (kind === 'notice' && slots.length > 0)
211
+ throw new Error('a notice takes no response: it has no slots');
206
212
  const steps = question.steps ?? 1;
207
- this.questions.set(id, { question_id: id, title: question.title, ...(question.subtitle === undefined ? {} : { subtitle: question.subtitle }), steps, slots, status: 'open' });
213
+ this.questions.set(id, { question_id: id, kind, title: question.title, ...(question.subtitle === undefined ? {} : { subtitle: question.subtitle }), steps, slots, status: 'open' });
214
+ if (kind === 'notice')
215
+ return id;
208
216
  this.rested = true;
209
217
  this.append('waiting_on_user', {
210
218
  question_id: id, initiating_app: this.host.grantee, title: question.title,
@@ -256,13 +264,17 @@ export class FakeRun {
256
264
  this.answered = true;
257
265
  this.append('question.answered', { question_id: questionId, answer: responses });
258
266
  }
267
+ /** The app canceled a question: the person dismissed it. */
268
+ cancelQuestion(questionId) {
269
+ this.questions.get(questionId).status = 'canceled';
270
+ }
259
271
  questionObject(questionId) {
260
272
  const q = this.questions.get(questionId);
261
273
  if (!q)
262
274
  return undefined;
263
275
  return {
264
276
  question_id: q.question_id, initiating_app: this.host.grantee, title: q.title, ...(q.subtitle === undefined ? {} : { subtitle: q.subtitle }),
265
- steps: q.steps, slots: q.slots, run_id: this.id, node_id: this.id, status: q.status, ...(q.answer ? { answer: q.answer } : {}),
277
+ steps: q.steps, slots: q.slots, run_id: this.id, node_id: this.id, kind: q.kind, status: q.status, ...(q.answer ? { answer: q.answer } : {}),
266
278
  };
267
279
  }
268
280
  /** Resolves when the run rests (`idle`, `waiting_on_user`, `settled`), is deleted, `seconds` pass, or `signal` aborts. */
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.515";
2
+ export declare const SDK_VERSION = "0.3.519";
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.515';
3
+ export const SDK_VERSION = '0.3.519';
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.515",
4
- "crouterContract": "1.23",
3
+ "version": "0.3.519",
4
+ "crouterContract": "1.24",
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,8 +44,8 @@
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.515",
48
- "@north-light/identity": "0.3.515",
47
+ "@north-light/api": "0.3.519",
48
+ "@north-light/identity": "0.3.519",
49
49
  "jose": "^6.2.1"
50
50
  },
51
51
  "peerDependencies": {