broapp 0.4.2 → 0.4.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "broapp",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "description": "Build tooling and runtime for local applications made of a Bun host, a browser UI, and a Brobridge connection between them",
6
6
  "license": "MIT",
@@ -49,9 +49,9 @@
49
49
  "./package.json": "./package.json"
50
50
  },
51
51
  "dependencies": {
52
- "@brobridgejs/client": "^0.2.1",
53
- "@brobridgejs/core": "^0.2.1",
54
- "brobridge": "^0.2.1"
52
+ "@brobridgejs/client": "^0.2.2",
53
+ "@brobridgejs/core": "^0.2.2",
54
+ "brobridge": "^0.2.2"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "react": ">=18",
@@ -71,6 +71,18 @@ export interface InProcessTurn {
71
71
  readonly history?: readonly ChatTurn[];
72
72
  }
73
73
 
74
+ /** One question put to an in-process turn's stand-in. */
75
+ export interface InProcessQuestion {
76
+ readonly tool: string;
77
+ readonly input: unknown;
78
+ /** The approval table's key, `<runId>:<callId>`. */
79
+ readonly requestId: string;
80
+ /** The call the question is about, as `ai.chatConfirm` names it with the run id. */
81
+ readonly callId: string;
82
+ /** When the question stops waiting, when the gate said. */
83
+ readonly expiresAt?: number;
84
+ }
85
+
74
86
  /** How {@link Ai.turn} is answered and stopped. */
75
87
  export interface InProcessTurnOptions {
76
88
  /**
@@ -78,8 +90,14 @@ export interface InProcessTurnOptions {
78
90
  *
79
91
  * The caller is the person's stand-in, so it decides exactly as the person
80
92
  * would have: the gate still asks, and still records the answer.
93
+ *
94
+ * `'defer'` answers nothing. The question stays in the approval table for
95
+ * somebody else to answer by its request id, over `ai.chatConfirm` as a
96
+ * person in a chat would, and the gate's own window still ends it. That is
97
+ * for a stand-in that answers some questions itself and brings the rest to a
98
+ * person: whoever answers, each question is answered once.
81
99
  */
82
- readonly answer: (question: { readonly tool: string; readonly input: unknown }) => boolean;
100
+ readonly answer: (question: InProcessQuestion) => boolean | 'defer';
83
101
  /** Aborting it cancels the turn, as a browser's cancel would. */
84
102
  readonly signal?: AbortSignal;
85
103
  readonly onEvent?: (event: ChatEvent) => void;
@@ -398,7 +416,14 @@ export function createAi(options: CreateAiOptions): Ai {
398
416
  // The gate's request id is `<runId>:<callId>`, which is what an
399
417
  // event without one would have named.
400
418
  const requestId = event.requestId ?? `${turn.runId}:${event.callId}`;
401
- void settle(requestId, turnOptions.answer({ tool: event.tool ?? '', input: event.input }));
419
+ const answer = turnOptions.answer({
420
+ tool: event.tool ?? '',
421
+ input: event.input,
422
+ requestId,
423
+ callId: event.callId ?? '',
424
+ ...(event.expiresAt === undefined ? {} : { expiresAt: event.expiresAt }),
425
+ });
426
+ if (answer !== 'defer') void settle(requestId, answer);
402
427
  }
403
428
  return Promise.resolve();
404
429
  },
@@ -12,6 +12,7 @@ export type {
12
12
  AiAppDescription,
13
13
  CreateAiOptions,
14
14
  DeliveredContext,
15
+ InProcessQuestion,
15
16
  InProcessTurn,
16
17
  InProcessTurnOptions,
17
18
  InProcessTurnResult,
@@ -288,14 +288,22 @@ function boundOutput(output: unknown, max: number): unknown {
288
288
  return { type: 'text', value: cut(json, max) };
289
289
  }
290
290
 
291
- /** One transcript message with every tool input and output bounded. */
291
+ /**
292
+ * One transcript message with every tool input and output bounded.
293
+ *
294
+ * A cut input stays an object, `{ truncated: "<head><omitted N chars>" }`,
295
+ * rather than becoming the string itself: an OpenAI-compatible provider sends
296
+ * a call's input as its `arguments`, and Ollama refuses a whole request whose
297
+ * arguments are not a JSON object ("invalid tool call arguments"). The first
298
+ * 12j evaluation lost a turn to exactly that.
299
+ */
292
300
  function boundMessage(message: ResponseMessage, limits: HistoryLimits): ModelMessage {
293
301
  if (!Array.isArray(message.content)) return message;
294
302
  const content = (message.content as readonly unknown[]).map((part) => {
295
303
  if (!isRecord(part)) return part;
296
304
  if (part['type'] === 'tool-call') {
297
305
  const json = JSON.stringify(part['input']) ?? '';
298
- return json.length <= limits.inputChars ? part : { ...part, input: cut(json, limits.inputChars) };
306
+ return json.length <= limits.inputChars ? part : { ...part, input: { truncated: cut(json, limits.inputChars) } };
299
307
  }
300
308
  if (part['type'] === 'tool-result') return { ...part, output: boundOutput(part['output'], limits.outputChars) };
301
309
  return part;
@@ -76,6 +76,15 @@ export interface RunningApp {
76
76
  * looking attached for that whole minute.
77
77
  */
78
78
  readonly attached: boolean;
79
+ /**
80
+ * A fresh address for this application, carrying a new one-time launch token.
81
+ *
82
+ * Brobridge mints the token; each address works once and expires on the
83
+ * bridge's launch-token lifetime. Call it on the host's own decision — a
84
+ * person's click in an authenticated tab, a local process that already holds
85
+ * a credential — never because a browser asked. Do not log the result.
86
+ */
87
+ launchUrl(): string;
79
88
  }
80
89
 
81
90
  const POLL_INTERVAL_MS = 1_000;
@@ -215,5 +224,6 @@ export async function startApp(options: StartAppOptions): Promise<RunningApp> {
215
224
  get attached() {
216
225
  return isAttached();
217
226
  },
227
+ launchUrl: () => bridge.launchUrl(),
218
228
  };
219
229
  }