@giovannijecha/jecode 0.8.6 → 0.8.7

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.
Files changed (67) hide show
  1. package/README.md +18 -8
  2. package/assets/tokenizers/LICENSE +21 -0
  3. package/assets/tokenizers/o200k-base.tiktoken.gz +0 -0
  4. package/dist/cli-info.js +8 -13
  5. package/dist/commands.js +7 -3
  6. package/dist/config.js +33 -47
  7. package/dist/context/automatic.js +16 -1
  8. package/dist/context/budget.js +5 -2
  9. package/dist/context/compactor.js +91 -37
  10. package/dist/context/diagnostics.js +108 -0
  11. package/dist/context/lifetime.js +18 -0
  12. package/dist/context/manager.js +67 -0
  13. package/dist/context/manual.js +11 -10
  14. package/dist/context/measurement.js +75 -0
  15. package/dist/context/policy.js +13 -10
  16. package/dist/context/request-observation.js +46 -0
  17. package/dist/context/request-projection.js +8 -34
  18. package/dist/context/request.js +22 -0
  19. package/dist/context/tokenizer/bpe.js +68 -0
  20. package/dist/context/tokenizer/o200k.js +54 -0
  21. package/dist/context/tokenizer/vocabulary.js +34 -0
  22. package/dist/controller-request.js +47 -45
  23. package/dist/controller.js +13 -2
  24. package/dist/input-boundary.js +0 -62
  25. package/dist/launch.js +13 -9
  26. package/dist/model-command.js +7 -17
  27. package/dist/oauth-result-page.js +68 -0
  28. package/dist/openai-oauth-callback.js +2 -50
  29. package/dist/permission-command.js +9 -24
  30. package/dist/permissions.js +10 -8
  31. package/dist/providers/anthropic-wire.js +1 -1
  32. package/dist/providers/anthropic.js +3 -1
  33. package/dist/providers/input-measurement.js +85 -0
  34. package/dist/providers/ollama-wire.js +1 -1
  35. package/dist/providers/ollama.js +2 -0
  36. package/dist/providers/openai-codex.js +3 -0
  37. package/dist/providers/openai-stream.js +20 -8
  38. package/dist/providers/openai-summary.js +37 -0
  39. package/dist/providers/openai-wire.js +1 -1
  40. package/dist/providers/openai.js +3 -0
  41. package/dist/settings-command.js +5 -10
  42. package/dist/settings.js +1 -1
  43. package/dist/start.js +35 -51
  44. package/dist/timeline.js +2 -0
  45. package/dist/tui/activity.js +1 -1
  46. package/dist/tui/app-input.js +34 -17
  47. package/dist/tui/app-workflows.js +7 -2
  48. package/dist/tui/app.js +55 -28
  49. package/dist/tui/approve.js +3 -4
  50. package/dist/tui/blocks.js +5 -7
  51. package/dist/tui/command-workflow.js +14 -8
  52. package/dist/tui/components/command-menu.js +1 -1
  53. package/dist/tui/components/menu.js +19 -11
  54. package/dist/tui/components/status.js +1 -1
  55. package/dist/tui/frame.js +8 -3
  56. package/dist/tui/help.js +2 -1
  57. package/dist/tui/picker-layout.js +5 -1
  58. package/dist/tui/screen.js +7 -0
  59. package/dist/tui/session-view.js +0 -1
  60. package/dist/tui/transcript-grammar.js +1 -8
  61. package/dist/tui/transcript-view.js +4 -0
  62. package/dist/tui/turn-workflow.js +47 -76
  63. package/dist/tui/view.js +4 -4
  64. package/dist/ui/render.js +0 -4
  65. package/package.json +5 -1
  66. package/dist/batch-view.js +0 -42
  67. package/dist/batch.js +0 -269
@@ -1,9 +1,7 @@
1
1
  // One model turn, including steering, compaction, durable settlement, and recovery.
2
2
  import { runTurn } from "../controller.js";
3
3
  import { resolveContextPolicy } from "../context/capacity.js";
4
- import { automaticCompactionKey } from "../context/automatic.js";
5
- import { compactContext } from "../context/compactor.js";
6
- import { isContextOverflow } from "../context/policy.js";
4
+ import { contextManager } from "../context/manager.js";
7
5
  import { steeringInbox } from "../steering.js";
8
6
  import { recordAuxiliaryUsage, recordRequestInput, recordUsage } from "../usage.js";
9
7
  import { toolSpecs } from "../tools/index.js";
@@ -11,16 +9,21 @@ import { requestIdentityForSession } from "../request-identity.js";
11
9
  import { transition } from "./activity.js";
12
10
  import { answerAt } from "./approve.js";
13
11
  import * as edit from "./editor.js";
14
- import { controllerOptions, turnFailure } from "./session-view.js";
12
+ import { controllerOptions, footerInfo, turnFailure } from "./session-view.js";
15
13
  import { transcribe } from "./turn.js";
16
14
  const WAITING = "Waiting";
17
- export function turnWorkflow(options, automaticCompaction) {
15
+ export function turnWorkflow(options, automaticCompaction, inputs) {
18
16
  const { session, state, permissions, feedback } = options;
19
17
  let activeSteering;
20
18
  async function turn(text) {
21
19
  const activity = options.startActivity("turn", WAITING);
22
20
  if (activity === undefined)
23
21
  return;
22
+ // Settings menus remain live, but every request, compaction, and checkpoint
23
+ // in this turn must retain the selection with which the turn started.
24
+ const runtime = { ...session, config: { ...session.config } };
25
+ const requestIdentity = requestIdentityForSession(session);
26
+ state.turnFooter = footerInfo(runtime);
24
27
  const inbox = steeringInbox((pending, accepting) => {
25
28
  if (activeSteering !== inbox)
26
29
  return;
@@ -51,9 +54,9 @@ export function turnWorkflow(options, automaticCompaction) {
51
54
  options.render();
52
55
  }
53
56
  return resolveContextPolicy({
54
- provider: session.provider,
55
- model: session.model,
56
- compactionPercent: session.config.compactionPercent,
57
+ provider: runtime.provider,
58
+ model: runtime.model,
59
+ compactionPercent: runtime.config.compactionPercent,
57
60
  signal: activity.control.signal,
58
61
  onStatus: (said) => {
59
62
  visible = true;
@@ -67,6 +70,11 @@ export function turnWorkflow(options, automaticCompaction) {
67
70
  }
68
71
  });
69
72
  };
73
+ const requestOptions = {
74
+ ...controllerOptions(session, policy, turnTools, inbox),
75
+ inputMeter: inputs.forTurn(runtime.provider, requestIdentity.conversationId),
76
+ toolAllowed: (call) => permissions.allowed(call),
77
+ };
70
78
  options.emit({ kind: "user", text });
71
79
  const user = { role: "user", content: [{ kind: "text", text }] };
72
80
  history.push(user);
@@ -78,7 +86,7 @@ export function turnWorkflow(options, automaticCompaction) {
78
86
  approved: (call) => permissions.approved(call),
79
87
  remember: (call) => permissions.remember(call),
80
88
  ask: (prompt, settle) => {
81
- state.open = { picker: prompt, settle: (index) => settle(answerAt(index)) };
89
+ state.approval = { picker: prompt, settle: (index) => settle(answerAt(index)) };
82
90
  options.render();
83
91
  },
84
92
  status: (text) => {
@@ -93,9 +101,9 @@ export function turnWorkflow(options, automaticCompaction) {
93
101
  parentId,
94
102
  createdAt,
95
103
  identity: {
96
- providerId: session.provider.id,
97
- model: session.model,
98
- effort: session.config.effort,
104
+ providerId: runtime.provider.id,
105
+ model: runtime.model,
106
+ effort: runtime.config.effort,
99
107
  },
100
108
  messages: checkpoint.slice(historyStart),
101
109
  blocks: state.blocks.slice(blockStart),
@@ -108,86 +116,49 @@ export function turnWorkflow(options, automaticCompaction) {
108
116
  state.committedNodeId = next.activeNodeId;
109
117
  unpersistedSteering.length = 0;
110
118
  };
111
- const compact = async (checkpoint, projected, request) => {
112
- if (request.reason === "overflow" &&
113
- (request.error === undefined || !isContextOverflow(request.error))) {
114
- return undefined;
115
- }
116
- const force = request.reason === "overflow" || request.projectionSaturated;
117
- const key = automaticCompactionKey(session.provider.id, session.model, nodeId ?? prospectiveNodeId, checkpoint.length);
118
- const attempt = { key, reason: request.reason };
119
- if (!automaticCompaction.allows(attempt))
120
- return undefined;
121
- let attempted = false;
122
- const result = await compactContext({
123
- provider: session.provider,
124
- model: session.model,
125
- effort: session.config.effort,
126
- context: projected,
127
- turn: checkpoint.slice(historyStart),
128
- nodeId: nodeId ?? prospectiveNodeId,
129
- coveredMessages: context?.messageCount ?? 0,
130
- lastInputTokens: Math.max(session.usage.lastInputTokens, request.inputTokens),
131
- estimatedInputTokens: request.inputTokens,
132
- signal: activity.control.signal,
133
- force,
134
- policy: request.policy,
135
- requestEnvelope: {
136
- system: session.system,
137
- tools: specs,
138
- maxOutputTokens: session.config.maxTokens,
139
- },
140
- requestIdentity: requestIdentityForSession(session),
141
- onBegin: () => {
142
- attempted = true;
143
- status("Compacting");
144
- options.render();
145
- },
146
- onEnd: () => {
147
- status(WAITING);
148
- options.render();
149
- },
150
- });
151
- if (result === undefined) {
152
- if (attempted && !activity.control.signal.aborted)
153
- automaticCompaction.failed(attempt);
154
- return undefined;
155
- }
156
- automaticCompaction.succeeded(attempt);
157
- context = result.anchor;
158
- if (result.usage !== undefined)
159
- recordAuxiliaryUsage(session.usage, result.usage);
160
- return result.messages;
119
+ const manager = contextManager({
120
+ provider: runtime.provider,
121
+ model: runtime.model,
122
+ effort: runtime.config.effort,
123
+ system: runtime.system,
124
+ tools: specs,
125
+ maxOutputTokens: runtime.config.maxTokens,
126
+ historyStart,
127
+ nodeId: () => nodeId ?? prospectiveNodeId,
128
+ gate: automaticCompaction,
129
+ identity: requestIdentity,
130
+ signal: activity.control.signal,
131
+ onStatus(active) {
132
+ status(active ? "Compacting" : WAITING);
133
+ options.render();
134
+ },
135
+ onUsage: (usage) => recordAuxiliaryUsage(session.usage, usage),
136
+ });
137
+ events.onContext = async (checkpoint, projected, request) => {
138
+ const result = await manager.compact(checkpoint, projected, request);
139
+ context = manager.anchor;
140
+ return result;
161
141
  };
162
- events.onContext = compact;
163
142
  events.onSteering = (guidance) => {
164
143
  unpersistedSteering.push(guidance);
165
144
  options.emit({ kind: "user", text: guidance });
166
145
  options.render();
167
146
  };
168
- events.onCheckpoint = async (checkpoint, settlement, projected) => {
147
+ events.onCheckpoint = async (checkpoint, settlement) => {
169
148
  await persist(checkpoint, settlement);
170
- const compacted = await compact(checkpoint, projected, {
171
- reason: "budget",
172
- policy: await policy(),
173
- inputTokens: session.usage.lastInputTokens,
174
- projectionSaturated: false,
175
- });
176
- if (compacted !== undefined)
177
- await persist(checkpoint, settlement);
178
- return compacted;
179
149
  };
180
150
  let finishReason;
181
151
  let failed;
182
152
  try {
183
- await runTurn(history, controllerOptions(session, policy, turnTools, inbox), events, activity.control.signal, modelHistory);
153
+ await runTurn(history, requestOptions, events, activity.control.signal, modelHistory);
184
154
  }
185
155
  catch (error) {
156
+ inputs.reset();
186
157
  const interrupted = activity.control.signal.aborted;
187
158
  const completed = nodeId !== undefined && session.conversation.activeNodeId === nodeId &&
188
159
  session.conversation.activeNode?.settlement === "completed";
189
160
  if (completed) {
190
- const notice = turnFailure(session, error, interrupted);
161
+ const notice = turnFailure(runtime, error, interrupted);
191
162
  feedback.show({ text: notice.text, tone: notice.tone, timeoutMs: 6_000 });
192
163
  }
193
164
  else {
@@ -203,7 +174,7 @@ export function turnWorkflow(options, automaticCompaction) {
203
174
  try {
204
175
  events.finish(finishReason);
205
176
  if (failed !== undefined) {
206
- const notice = turnFailure(session, failed.error, failed.interrupted);
177
+ const notice = turnFailure(runtime, failed.error, failed.interrupted);
207
178
  const settlement = failed.interrupted ? "interrupted" : "failed";
208
179
  const failure = {
209
180
  text: notice.text,
package/dist/tui/view.js CHANGED
@@ -39,10 +39,10 @@ export function compose(view, size, transcript = transcriptRenderer()) {
39
39
  }
40
40
  function dockRows(view, width, height) {
41
41
  const status = renderStatus({
42
- // A modal already names the interaction in progress. Let command feedback
43
- // use the footer while that interaction is open instead of repeating a
44
- // generic "Running /…" label beside it.
45
- status: view.modal === undefined ? view.status : undefined,
42
+ // A modal names its command; background model work still needs visible
43
+ // status. Its interruption/steering keys are suspended while a menu owns input.
44
+ status: view.modal === undefined || view.turnActive ? view.status : undefined,
45
+ interruptible: view.modal === undefined,
46
46
  steering: view.modal === undefined ? view.steering : undefined,
47
47
  feedback: view.feedback,
48
48
  readiness: view.readiness,
package/dist/ui/render.js CHANGED
@@ -45,10 +45,6 @@ export function paint(seg) {
45
45
  export function plainLen(segs) {
46
46
  return segs.reduce((n, seg) => n + textWidth(terminalText(seg.text)), 0);
47
47
  }
48
- export function columns() {
49
- const width = process.stdout.columns;
50
- return typeof width === "number" && width >= 40 ? width : 80;
51
- }
52
48
  /**
53
49
  * The margin every row keeps from the edges of the terminal: none.
54
50
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giovannijecha/jecode",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "An owned coding agent with zero external runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,8 @@
27
27
  "dist/",
28
28
  "assets/jeco-256.png",
29
29
  "assets/wordmark-steel.svg",
30
+ "assets/tokenizers/o200k-base.tiktoken.gz",
31
+ "assets/tokenizers/LICENSE",
30
32
  "LICENSE",
31
33
  "README.md"
32
34
  ],
@@ -45,11 +47,13 @@
45
47
  "pack:release": "npm run build:release && npm pack --ignore-scripts",
46
48
  "start": "node src/main.ts",
47
49
  "tui:lab": "node dev/tui/main.ts",
50
+ "web:preview": "node dev/web/main.ts",
48
51
  "bench:context": "node dev/benchmarks/context.ts",
49
52
  "bench:redaction": "node dev/benchmarks/redaction.ts",
50
53
  "bench:search": "node dev/benchmarks/search.ts",
51
54
  "bench:session": "node dev/benchmarks/session.ts",
52
55
  "bench:transcript": "node dev/benchmarks/transcript.ts",
56
+ "bench:tui": "node --expose-gc dev/benchmarks/tui.ts",
53
57
  "typecheck": "tsc --noEmit",
54
58
  "test": "npm run build:release && node --test",
55
59
  "coverage": "npm run build:release && node --test --experimental-test-coverage --test-coverage-include=\"src/**/*.ts\" --test-coverage-lines=80 --test-coverage-branches=75 --test-coverage-functions=75",
@@ -1,42 +0,0 @@
1
- // Project semantic blocks into a compact transcript for pipes.
2
- import { renderAnswer, renderReasoning, renderUser } from "./tui/components/messages.js";
3
- import { renderNotice } from "./tui/components/misc.js";
4
- import { renderDetail } from "./tui/components/tool-evidence.js";
5
- import { stateInk, stateMark } from "./tui/components/tool-motion.js";
6
- import { toolDuration } from "./duration.js";
7
- import { row } from "./ui/render.js";
8
- export function renderBatch(block, width, pal) {
9
- const rows = [];
10
- let lastWasBlank = false;
11
- for (const rendered of batchRows(block, width, pal)) {
12
- const line = rendered.trimEnd();
13
- const blank = line === "";
14
- if (blank && lastWasBlank)
15
- continue;
16
- rows.push(line);
17
- lastWasBlank = blank;
18
- }
19
- while (rows.at(-1) === "")
20
- rows.pop();
21
- return rows;
22
- }
23
- function batchRows(block, width, pal) {
24
- switch (block.kind) {
25
- case "answer": return renderAnswer(block, width, pal);
26
- case "reasoning": return renderReasoning(block, width, pal);
27
- case "user": return renderUser(block, width, pal);
28
- case "notice": return renderNotice(block, width, pal);
29
- case "tool": {
30
- // Pipes cannot expand records or repaint a running connector.
31
- const mark = stateMark(block);
32
- const result = [block.right, block.durationMs === undefined ? "" : toolDuration(block.durationMs)]
33
- .filter(Boolean).join(" · ");
34
- return ["", row(width, [
35
- { text: `${mark} `, fg: stateInk(block, pal) },
36
- { text: block.name, fg: pal.ink.bright },
37
- { text: block.target === "" ? "" : ` ${block.target}`, fg: pal.technical },
38
- ], [{ text: result, fg: pal.ink.muted }]),
39
- ...(block.body ?? []).map((detail) => renderDetail(detail, block.tone, width, pal))];
40
- }
41
- }
42
- }
package/dist/batch.js DELETED
@@ -1,269 +0,0 @@
1
- // The non-interactive path: stdin is a pipe, so there is no screen to own.
2
- //
3
- // It exists so the agent can be scripted and tested. It shares the controller,
4
- // the tools and the block renderer with the TUI — only the surface differs.
5
- import { stdin, stdout } from "node:process";
6
- import { runTurn } from "./controller.js";
7
- import { resolveContextPolicy } from "./context/capacity.js";
8
- import { automaticCompactionGate, automaticCompactionKey, } from "./context/automatic.js";
9
- import { compactContext } from "./context/compactor.js";
10
- import { compactSession } from "./context/manual.js";
11
- import { isContextOverflow } from "./context/policy.js";
12
- import { handleCommand } from "./commands.js";
13
- import { renderBatch } from "./batch-view.js";
14
- import { columns } from "./ui/render.js";
15
- import { terminalText } from "./ui/terminal-text.js";
16
- import { recordAuxiliaryUsage, recordRequestInput, recordUsage } from "./usage.js";
17
- import { assertPromptLength, boundedInputLines } from "./input-boundary.js";
18
- import { providerFailure } from "./provider-errors.js";
19
- import { toolSpecs } from "./tools/index.js";
20
- import { requestIdentityForSession } from "./request-identity.js";
21
- export async function runBatch(session, environment = {}) {
22
- const write = environment.write ?? ((text) => stdout.write(text));
23
- const width = environment.width ?? columns();
24
- const signal = environment.signal;
25
- const source = environment.lines ?? boundedInputLines(stdin);
26
- const lines = abortableLines(source, signal);
27
- const automaticCompaction = automaticCompactionGate();
28
- const emit = (block) => {
29
- for (const line of renderBatch(block, width, session.palette))
30
- write(`${line}\n`);
31
- };
32
- try {
33
- throwIfAborted(signal);
34
- for await (const raw of lines) {
35
- throwIfAborted(signal);
36
- assertPromptLength(raw.length);
37
- const line = raw.trim();
38
- if (line === "")
39
- continue;
40
- if (line.startsWith("/")) {
41
- if ((await handleCommand(line, session, {
42
- emit,
43
- signal,
44
- reset: () => {
45
- automaticCompaction.reset();
46
- },
47
- compact: async () => {
48
- const result = await compactSession(session, { signal });
49
- if (result === "compacted")
50
- automaticCompaction.reset();
51
- return result;
52
- },
53
- })) === "exit")
54
- break;
55
- continue;
56
- }
57
- write(`> ${terminalText(line)}\n`);
58
- const parentId = session.conversation.activeNodeId;
59
- const createdAt = new Date().toISOString();
60
- const history = session.conversation.history;
61
- const modelHistory = session.conversation.contextHistory;
62
- const before = history.length;
63
- const prospectiveNodeId = session.conversation.nodes.length + 1;
64
- let nodeId;
65
- let context;
66
- const policy = () => {
67
- return resolveContextPolicy({
68
- provider: session.provider,
69
- model: session.model,
70
- compactionPercent: session.config.compactionPercent,
71
- signal,
72
- });
73
- };
74
- const user = { role: "user", content: [{ kind: "text", text: line }] };
75
- history.push(user);
76
- modelHistory.push(structuredClone(user));
77
- const turn = events(emit, session);
78
- const specs = toolSpecs(session.tools);
79
- const commit = (checkpoint, settlement) => {
80
- session.conversation = session.conversation.commit({
81
- ...(nodeId === undefined ? {} : { nodeId }),
82
- parentId,
83
- createdAt,
84
- identity: {
85
- providerId: session.provider.id,
86
- model: session.model,
87
- effort: session.config.effort,
88
- },
89
- messages: checkpoint.slice(before),
90
- blocks: [],
91
- ...(context === undefined ? {} : { context }),
92
- }, settlement);
93
- nodeId = session.conversation.activeNodeId;
94
- };
95
- const compact = async (checkpoint, projected, request) => {
96
- if (request.reason === "overflow" &&
97
- (request.error === undefined || !isContextOverflow(request.error))) {
98
- return undefined;
99
- }
100
- const force = request.reason === "overflow" || request.projectionSaturated;
101
- const key = automaticCompactionKey(session.provider.id, session.model, nodeId ?? prospectiveNodeId, checkpoint.length);
102
- const attempt = { key, reason: request.reason };
103
- if (!automaticCompaction.allows(attempt))
104
- return undefined;
105
- let attempted = false;
106
- const result = await compactContext({
107
- provider: session.provider,
108
- model: session.model,
109
- effort: session.config.effort,
110
- signal,
111
- context: projected,
112
- turn: checkpoint.slice(before),
113
- nodeId: nodeId ?? prospectiveNodeId,
114
- coveredMessages: context?.messageCount ?? 0,
115
- lastInputTokens: Math.max(session.usage.lastInputTokens, request.inputTokens),
116
- estimatedInputTokens: request.inputTokens,
117
- force,
118
- policy: request.policy,
119
- requestEnvelope: {
120
- system: session.system,
121
- tools: specs,
122
- maxOutputTokens: session.config.maxTokens,
123
- },
124
- requestIdentity: requestIdentityForSession(session),
125
- onBegin: () => {
126
- attempted = true;
127
- },
128
- });
129
- if (result === undefined) {
130
- if (attempted && signal?.aborted !== true)
131
- automaticCompaction.failed(attempt);
132
- return undefined;
133
- }
134
- automaticCompaction.succeeded(attempt);
135
- context = result.anchor;
136
- if (result.usage !== undefined)
137
- recordAuxiliaryUsage(session.usage, result.usage);
138
- return result.messages;
139
- };
140
- turn.onContext = compact;
141
- turn.onCheckpoint = async (checkpoint, settlement, projected) => {
142
- commit(checkpoint, settlement);
143
- const compacted = await compact(checkpoint, projected, {
144
- reason: "budget",
145
- policy: await policy(),
146
- inputTokens: session.usage.lastInputTokens,
147
- projectionSaturated: false,
148
- });
149
- if (compacted !== undefined)
150
- commit(checkpoint, settlement);
151
- return compacted;
152
- };
153
- try {
154
- await runTurn(history, options(session, policy), turn, signal, modelHistory);
155
- }
156
- catch (error) {
157
- throwIfAborted(signal);
158
- if (!(error instanceof Error))
159
- throw error;
160
- const message = providerFailure(session.provider, error, true);
161
- if (message === error.message)
162
- throw error;
163
- throw new Error(message, { cause: error });
164
- }
165
- turn.flush();
166
- }
167
- throwIfAborted(signal);
168
- }
169
- finally {
170
- if (environment.lines === undefined)
171
- stdin.pause();
172
- }
173
- }
174
- function throwIfAborted(signal) {
175
- if (signal?.aborted === true)
176
- throw signal.reason;
177
- }
178
- async function* abortableLines(source, signal) {
179
- const iterator = source[Symbol.asyncIterator]();
180
- let exhausted = false;
181
- try {
182
- while (true) {
183
- throwIfAborted(signal);
184
- const next = signal === undefined
185
- ? await iterator.next()
186
- : await new Promise((resolve, reject) => {
187
- const abort = () => reject(signal.reason);
188
- signal.addEventListener("abort", abort, { once: true });
189
- iterator.next().then((result) => {
190
- signal.removeEventListener("abort", abort);
191
- resolve(result);
192
- }, (error) => {
193
- signal.removeEventListener("abort", abort);
194
- reject(error);
195
- });
196
- });
197
- if (next.done === true) {
198
- exhausted = true;
199
- return;
200
- }
201
- yield next.value;
202
- }
203
- }
204
- finally {
205
- if (!exhausted && iterator.return !== undefined) {
206
- const closing = iterator.return();
207
- if (signal?.aborted === true)
208
- void closing.catch(() => { });
209
- else
210
- await closing;
211
- }
212
- }
213
- }
214
- function options(session, contextPolicy) {
215
- return {
216
- provider: session.provider,
217
- tools: session.tools,
218
- model: session.model,
219
- system: session.system,
220
- maxTokens: session.config.maxTokens,
221
- contextPolicy,
222
- effort: session.config.effort,
223
- requestIdentity: requestIdentityForSession(session),
224
- maxModelRequests: session.config.maxModelRequests,
225
- toolContext: { root: session.config.root },
226
- };
227
- }
228
- // Prose is buffered rather than streamed: without a screen to repaint there is
229
- // nothing to gain from partial lines, and plenty to lose in readability.
230
- function events(emit, session) {
231
- let answer = "";
232
- const flush = () => {
233
- if (answer === "")
234
- return;
235
- emit({ kind: "answer", text: answer });
236
- answer = "";
237
- };
238
- return {
239
- flush,
240
- onStream(event) {
241
- if (event.kind === "text")
242
- answer += event.text;
243
- },
244
- onToolCall() {
245
- flush();
246
- },
247
- onToolResult(call, result, summary) {
248
- emit({
249
- kind: "tool",
250
- name: call.name,
251
- target: "",
252
- right: summary ?? "",
253
- tone: result.isError ? "fail" : "ok",
254
- });
255
- },
256
- approve() {
257
- flush();
258
- // Nobody is watching a pipe. Approval has to be granted up front with
259
- // --auto-approve, never inferred from silence.
260
- return Promise.resolve(session.config.autoApprove);
261
- },
262
- onUsage(usage) {
263
- recordUsage(session.usage, usage);
264
- },
265
- onRequestInput(inputTokens) {
266
- recordRequestInput(session.usage, inputTokens);
267
- },
268
- };
269
- }