@adecore/agents 0.36.0 → 0.37.1

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.
@@ -473,8 +473,11 @@ export class CodexBackend {
473
473
  }
474
474
  const turnId = this.protocol.turnId;
475
475
  if (!turnId) {
476
- // Codex refuses a steer without the turn it is meant for, and there is none left to join.
477
- return false;
476
+ // An optional question can outlive its turn; its answer resumes the same chat as plain text.
477
+ this.turnUnnamed = true;
478
+ this.promptId = null;
479
+ this.request('turn/start', this.turnStartParams(textInput(answer.text)));
480
+ return true;
478
481
  }
479
482
  this.request('turn/steer', { threadId: this.threadId, expectedTurnId: turnId, input: textInput(answer.text) });
480
483
  return true;
@@ -484,6 +487,9 @@ export class CodexBackend {
484
487
  return answer?.kind === 'respond' ? this.respond(requestId, answer.rpcId, answer.result) : this.protocol.dismissQuestion(requestId);
485
488
  }
486
489
  declineRequest(requestId, _message) {
490
+ if (this.protocol.dismissQuestion(requestId)) {
491
+ return true;
492
+ }
487
493
  const answer = this.protocol.declineRequest(requestId);
488
494
  if (!this.transport || !answer) {
489
495
  return false;
@@ -237,7 +237,7 @@ export class CodexProtocol {
237
237
  const threadId = frame.method === 'thread/started' ? (isRecord(params.thread) ? str(params.thread.id) : null) : str(params.threadId);
238
238
  if (threadId !== null && this.ownThreadId !== null && threadId !== this.ownThreadId) {
239
239
  if (frame.method === 'turn/completed') {
240
- this.withdrawOwner(threadId, isRecord(params.turn) ? str(params.turn.id) : null, events);
240
+ this.withdrawOwner(threadId, isRecord(params.turn) ? str(params.turn.id) : null, events, isRecord(params.turn) && params.turn.status === 'completed');
241
241
  }
242
242
  else if (frame.method === 'thread/closed') {
243
243
  this.withdrawOwner(threadId, null, events);
@@ -367,7 +367,7 @@ export class CodexProtocol {
367
367
  this.pending.delete(requestId);
368
368
  return { rpcId: pending.rpcId, result: { decision: codexDecision(decision, pending) } };
369
369
  }
370
- /* What answers a question, the reply to a blocking one or the text that steers a running turn. */
370
+ /* An RPC reply, or plain text that continues the chat, for a question that still waits. */
371
371
  questionAnswer(requestId, answers) {
372
372
  const pending = this.pending.get(requestId);
373
373
  if (pending?.type !== 'question') {
@@ -437,10 +437,10 @@ export class CodexProtocol {
437
437
  this.toolInputs.delete(this.toolInputs.keys().next().value);
438
438
  }
439
439
  }
440
- withdrawOwner(threadId, turnId, events) {
440
+ withdrawOwner(threadId, turnId, events, keepAsyncQuestions = false) {
441
441
  const belongs = (owner) => owner.threadId === threadId && (turnId === null || owner.turnId === null || owner.turnId === turnId);
442
442
  for (const [requestId, pending] of this.pending) {
443
- if (belongs(pending)) {
443
+ if (belongs(pending) && !(keepAsyncQuestions && pending.type === 'question' && pending.async === true)) {
444
444
  this.pending.delete(requestId);
445
445
  events.push({ type: 'request.withdrawn', requestId });
446
446
  }
@@ -598,7 +598,13 @@ export class CodexProtocol {
598
598
  if (!completed || this.pending.has(ref)) {
599
599
  return;
600
600
  }
601
- this.pending.set(ref, { ...this.owner(params), type: 'question', rpcId: null, questionIds: questions.map((question) => question.id) });
601
+ this.pending.set(ref, {
602
+ ...this.owner(params),
603
+ type: 'question',
604
+ rpcId: null,
605
+ questionIds: questions.map((question) => question.id),
606
+ async: true
607
+ });
602
608
  events.push({ type: 'question.requested', requestId: ref, questions, async: true });
603
609
  return;
604
610
  }
@@ -713,7 +719,7 @@ export class CodexProtocol {
713
719
  const limit = status === 'failed' && isRecord(turn.error) ? this.limitOf(turn.error.codexErrorInfo) : null;
714
720
  const turnId = str(turn.id) ?? this.codexTurnId;
715
721
  this.codexTurnId = null;
716
- this.withdrawOwner(str(params.threadId) ?? this.ownThreadId, turnId, events);
722
+ this.withdrawOwner(str(params.threadId) ?? this.ownThreadId, turnId, events, status === 'completed');
717
723
  // A terminal session still open as the turn ends is one Codex left running; it only completes once its process does.
718
724
  for (const [ref, processId] of this.openTerminals) {
719
725
  this.backgroundTerminals.set(ref, processId);
@@ -790,7 +796,7 @@ export class CodexProtocol {
790
796
  continue;
791
797
  }
792
798
  row.settled = true;
793
- this.withdrawOwner(threadId, null, events);
799
+ this.withdrawOwner(threadId, null, events, status === 'completed');
794
800
  settled = true;
795
801
  events.push({ type: 'task.done', ref: row.ref, summary: collabAgentOutput({ [threadId]: state }), ok: status === 'completed' });
796
802
  }
@@ -834,7 +840,7 @@ export class CodexProtocol {
834
840
  }
835
841
  if ((kind === 'completed' || kind === 'interrupted') && row !== undefined && !row.settled) {
836
842
  row.settled = true;
837
- this.withdrawOwner(threadId, null, events);
843
+ this.withdrawOwner(threadId, null, events, kind === 'completed');
838
844
  events.push({ type: 'task.done', ref: row.ref, summary: null, ok: kind === 'completed' });
839
845
  }
840
846
  }
@@ -5,6 +5,7 @@ export declare class ChatThread {
5
5
  private readonly order;
6
6
  private historyGeneration;
7
7
  private readonly indices;
8
+ private readonly requests;
8
9
  private readonly holds;
9
10
  constructor(info: ChatInfo, items?: ChatItem[]);
10
11
  get(id: string): ChatItem | undefined;
@@ -25,6 +26,7 @@ export declare class ChatThread {
25
26
  setStatus(status: AgentStatus): ChatEvent;
26
27
  statusFor(activeTurnId: string | null): AgentStatus;
27
28
  pending(): Array<ChatApprovalItem | ChatQuestionItem>;
29
+ private indexRequest;
28
30
  private syncRequests;
29
31
  history(limit?: number, cursor?: string): ChatHistoryResult;
30
32
  subagentsBefore(start: number): ChatSubagentItem[];
@@ -13,6 +13,7 @@ export class ChatThread {
13
13
  order = [];
14
14
  historyGeneration = randomUUID();
15
15
  indices = new Map();
16
+ requests = new Map();
16
17
  // What the host asks a person beside the CLI's own requests, such as a card a verb waits on.
17
18
  holds = new Set();
18
19
  constructor(info, items = []) {
@@ -21,6 +22,7 @@ export class ChatThread {
21
22
  this.items.set(item.id, item);
22
23
  this.indices.set(item.id, this.order.length);
23
24
  this.order.push(item.id);
25
+ this.indexRequest(item);
24
26
  }
25
27
  this.syncRequests();
26
28
  }
@@ -45,7 +47,9 @@ export class ChatThread {
45
47
  this.order.push(item.id);
46
48
  }
47
49
  this.items.set(item.id, item);
48
- if (item.kind === 'approval' || item.kind === 'question') {
50
+ const wasPending = this.requests.has(item.id);
51
+ this.indexRequest(item);
52
+ if (wasPending || item.kind === 'approval' || item.kind === 'question') {
49
53
  this.syncRequests();
50
54
  }
51
55
  return { type: 'item', item, historyIndex: this.indices.get(item.id) };
@@ -89,6 +93,7 @@ export class ChatThread {
89
93
  reset(patch) {
90
94
  this.items.clear();
91
95
  this.indices.clear();
96
+ this.requests.clear();
92
97
  this.historyGeneration = randomUUID();
93
98
  this.order.length = 0;
94
99
  this.info = { ...this.info, ...patch };
@@ -126,13 +131,21 @@ export class ChatThread {
126
131
  }
127
132
  /* A request can outlive the turn it came in, when work the CLI runs beside its turns asked it. */
128
133
  statusFor(activeTurnId) {
129
- if (this.pending().length > 0 || this.holds.size > 0) {
134
+ if (this.requests.size > 0 || this.holds.size > 0) {
130
135
  return 'needs-you';
131
136
  }
132
137
  return activeTurnId === null ? 'idle' : 'running';
133
138
  }
134
139
  pending() {
135
- return this.list().filter((item) => (item.kind === 'approval' && item.decision === 'pending') || (item.kind === 'question' && item.state === 'pending'));
140
+ return [...this.requests.values()].sort((left, right) => this.indices.get(left.id) - this.indices.get(right.id));
141
+ }
142
+ indexRequest(item) {
143
+ if ((item.kind === 'approval' && item.decision === 'pending') || (item.kind === 'question' && item.state === 'pending')) {
144
+ this.requests.set(item.id, item);
145
+ }
146
+ else {
147
+ this.requests.delete(item.id);
148
+ }
136
149
  }
137
150
  /*
138
151
  * `info.requests` follows the request items, so whatever event comes next carries what the chat waits on,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents",
3
- "version": "0.36.0",
3
+ "version": "0.37.1",
4
4
  "description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -81,8 +81,8 @@
81
81
  "test": "bun --conditions=source test"
82
82
  },
83
83
  "dependencies": {
84
- "@adecore/agent-contracts": "0.36.0",
85
- "@adecore/intelligent-ui": "0.36.0",
84
+ "@adecore/agent-contracts": "0.37.1",
85
+ "@adecore/intelligent-ui": "0.37.1",
86
86
  "zod": "^4.6.5"
87
87
  },
88
88
  "devDependencies": {
@@ -503,8 +503,11 @@ export class CodexBackend implements ChatBackend {
503
503
  }
504
504
  const turnId = this.protocol.turnId;
505
505
  if (!turnId) {
506
- // Codex refuses a steer without the turn it is meant for, and there is none left to join.
507
- return false;
506
+ // An optional question can outlive its turn; its answer resumes the same chat as plain text.
507
+ this.turnUnnamed = true;
508
+ this.promptId = null;
509
+ this.request('turn/start', this.turnStartParams(textInput(answer.text)));
510
+ return true;
508
511
  }
509
512
  this.request('turn/steer', { threadId: this.threadId, expectedTurnId: turnId, input: textInput(answer.text) });
510
513
  return true;
@@ -516,6 +519,9 @@ export class CodexBackend implements ChatBackend {
516
519
  }
517
520
 
518
521
  declineRequest(requestId: string, _message: string): boolean {
522
+ if (this.protocol.dismissQuestion(requestId)) {
523
+ return true;
524
+ }
519
525
  const answer = this.protocol.declineRequest(requestId);
520
526
  if (!this.transport || !answer) {
521
527
  return false;
@@ -285,7 +285,12 @@ export class CodexProtocol {
285
285
  const threadId = frame.method === 'thread/started' ? (isRecord(params.thread) ? str(params.thread.id) : null) : str(params.threadId);
286
286
  if (threadId !== null && this.ownThreadId !== null && threadId !== this.ownThreadId) {
287
287
  if (frame.method === 'turn/completed') {
288
- this.withdrawOwner(threadId, isRecord(params.turn) ? str(params.turn.id) : null, events);
288
+ this.withdrawOwner(
289
+ threadId,
290
+ isRecord(params.turn) ? str(params.turn.id) : null,
291
+ events,
292
+ isRecord(params.turn) && params.turn.status === 'completed'
293
+ );
289
294
  } else if (frame.method === 'thread/closed') {
290
295
  this.withdrawOwner(threadId, null, events);
291
296
  }
@@ -416,7 +421,7 @@ export class CodexProtocol {
416
421
  return { rpcId: pending.rpcId, result: { decision: codexDecision(decision, pending) } };
417
422
  }
418
423
 
419
- /* What answers a question, the reply to a blocking one or the text that steers a running turn. */
424
+ /* An RPC reply, or plain text that continues the chat, for a question that still waits. */
420
425
  questionAnswer(requestId: string, answers: Record<string, string>): CodexAnswer | null {
421
426
  const pending = this.pending.get(requestId);
422
427
  if (pending?.type !== 'question') {
@@ -494,10 +499,10 @@ export class CodexProtocol {
494
499
  }
495
500
  }
496
501
 
497
- private withdrawOwner(threadId: string | null, turnId: string | null, events: BackendEvent[]): void {
502
+ private withdrawOwner(threadId: string | null, turnId: string | null, events: BackendEvent[], keepAsyncQuestions = false): void {
498
503
  const belongs = (owner: RequestOwner): boolean => owner.threadId === threadId && (turnId === null || owner.turnId === null || owner.turnId === turnId);
499
504
  for (const [requestId, pending] of this.pending) {
500
- if (belongs(pending)) {
505
+ if (belongs(pending) && !(keepAsyncQuestions && pending.type === 'question' && pending.async === true)) {
501
506
  this.pending.delete(requestId);
502
507
  events.push({ type: 'request.withdrawn', requestId });
503
508
  }
@@ -664,7 +669,13 @@ export class CodexProtocol {
664
669
  if (!completed || this.pending.has(ref)) {
665
670
  return;
666
671
  }
667
- this.pending.set(ref, { ...this.owner(params), type: 'question', rpcId: null, questionIds: questions.map((question) => question.id) });
672
+ this.pending.set(ref, {
673
+ ...this.owner(params),
674
+ type: 'question',
675
+ rpcId: null,
676
+ questionIds: questions.map((question) => question.id),
677
+ async: true
678
+ });
668
679
  events.push({ type: 'question.requested', requestId: ref, questions, async: true });
669
680
  return;
670
681
  }
@@ -805,7 +816,7 @@ export class CodexProtocol {
805
816
  const limit = status === 'failed' && isRecord(turn.error) ? this.limitOf(turn.error.codexErrorInfo) : null;
806
817
  const turnId = str(turn.id) ?? this.codexTurnId;
807
818
  this.codexTurnId = null;
808
- this.withdrawOwner(str(params.threadId) ?? this.ownThreadId, turnId, events);
819
+ this.withdrawOwner(str(params.threadId) ?? this.ownThreadId, turnId, events, status === 'completed');
809
820
  // A terminal session still open as the turn ends is one Codex left running; it only completes once its process does.
810
821
  for (const [ref, processId] of this.openTerminals) {
811
822
  this.backgroundTerminals.set(ref, processId);
@@ -886,7 +897,7 @@ export class CodexProtocol {
886
897
  continue;
887
898
  }
888
899
  row.settled = true;
889
- this.withdrawOwner(threadId, null, events);
900
+ this.withdrawOwner(threadId, null, events, status === 'completed');
890
901
  settled = true;
891
902
  events.push({ type: 'task.done', ref: row.ref, summary: collabAgentOutput({ [threadId]: state }), ok: status === 'completed' });
892
903
  }
@@ -931,7 +942,7 @@ export class CodexProtocol {
931
942
  }
932
943
  if ((kind === 'completed' || kind === 'interrupted') && row !== undefined && !row.settled) {
933
944
  row.settled = true;
934
- this.withdrawOwner(threadId, null, events);
945
+ this.withdrawOwner(threadId, null, events, kind === 'completed');
935
946
  events.push({ type: 'task.done', ref: row.ref, summary: null, ok: kind === 'completed' });
936
947
  }
937
948
  }
@@ -24,6 +24,7 @@ export class ChatThread {
24
24
  private readonly order: string[] = [];
25
25
  private historyGeneration = randomUUID();
26
26
  private readonly indices = new Map<string, number>();
27
+ private readonly requests = new Map<string, ChatApprovalItem | ChatQuestionItem>();
27
28
  // What the host asks a person beside the CLI's own requests, such as a card a verb waits on.
28
29
  private readonly holds = new Set<string>();
29
30
 
@@ -33,6 +34,7 @@ export class ChatThread {
33
34
  this.items.set(item.id, item);
34
35
  this.indices.set(item.id, this.order.length);
35
36
  this.order.push(item.id);
37
+ this.indexRequest(item);
36
38
  }
37
39
  this.syncRequests();
38
40
  }
@@ -61,7 +63,9 @@ export class ChatThread {
61
63
  this.order.push(item.id);
62
64
  }
63
65
  this.items.set(item.id, item);
64
- if (item.kind === 'approval' || item.kind === 'question') {
66
+ const wasPending = this.requests.has(item.id);
67
+ this.indexRequest(item);
68
+ if (wasPending || item.kind === 'approval' || item.kind === 'question') {
65
69
  this.syncRequests();
66
70
  }
67
71
  return { type: 'item', item, historyIndex: this.indices.get(item.id)! };
@@ -111,6 +115,7 @@ export class ChatThread {
111
115
  reset(patch: Partial<ChatInfo>): ChatEvent {
112
116
  this.items.clear();
113
117
  this.indices.clear();
118
+ this.requests.clear();
114
119
  this.historyGeneration = randomUUID();
115
120
  this.order.length = 0;
116
121
  this.info = { ...this.info, ...patch };
@@ -151,17 +156,22 @@ export class ChatThread {
151
156
 
152
157
  /* A request can outlive the turn it came in, when work the CLI runs beside its turns asked it. */
153
158
  statusFor(activeTurnId: string | null): AgentStatus {
154
- if (this.pending().length > 0 || this.holds.size > 0) {
159
+ if (this.requests.size > 0 || this.holds.size > 0) {
155
160
  return 'needs-you';
156
161
  }
157
162
  return activeTurnId === null ? 'idle' : 'running';
158
163
  }
159
164
 
160
165
  pending(): Array<ChatApprovalItem | ChatQuestionItem> {
161
- return this.list().filter(
162
- (item): item is ChatApprovalItem | ChatQuestionItem =>
163
- (item.kind === 'approval' && item.decision === 'pending') || (item.kind === 'question' && item.state === 'pending')
164
- );
166
+ return [...this.requests.values()].sort((left, right) => this.indices.get(left.id)! - this.indices.get(right.id)!);
167
+ }
168
+
169
+ private indexRequest(item: ChatItem): void {
170
+ if ((item.kind === 'approval' && item.decision === 'pending') || (item.kind === 'question' && item.state === 'pending')) {
171
+ this.requests.set(item.id, item);
172
+ } else {
173
+ this.requests.delete(item.id);
174
+ }
165
175
  }
166
176
 
167
177
  /*