@adecore/agents 0.48.0 → 0.48.2

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.
@@ -22,6 +22,10 @@ function continuedNoteId(turnId) {
22
22
  function newId(prefix) {
23
23
  return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
24
24
  }
25
+ /* The note of what the agent heard in front of a prompt, which a person reads in the turn it opened. */
26
+ function saidNote(turnId, now, text) {
27
+ return { id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text, said: true };
28
+ }
25
29
  /*
26
30
  * Item and request ids carry the generation of the CLI that made them, and a restart of the host
27
31
  * must not hand out one a stored thread already holds. Starting from the clock puts every run above
@@ -834,7 +838,7 @@ export class ChatSession {
834
838
  const context = this.contextNote(words.prompt);
835
839
  const preamble = [words.preamble, context.preamble].filter(Boolean).join('\n\n') || null;
836
840
  if (context.note) {
837
- this.emit([this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: context.note })]);
841
+ this.emit([this.thread.upsert(saidNote(turnId, now, context.note))]);
838
842
  }
839
843
  const input = { promptId: randomUUID(), text: words.prompt, preamble, attachments: [], mentions: [], skills: [] };
840
844
  this.submission = { turnId, input };
@@ -900,7 +904,7 @@ export class ChatSession {
900
904
  ...(wake.note === undefined
901
905
  ? []
902
906
  : [this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: wake.note })]),
903
- ...(note === null ? [] : [this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: note })]),
907
+ ...(note === null ? [] : [this.thread.upsert(saidNote(turnId, now, note))]),
904
908
  this.thread.patchInfo({ status: this.thread.statusFor(turnId), activeTurnId: turnId })
905
909
  ]);
906
910
  this.options.persist();
@@ -1383,7 +1387,7 @@ export class ChatSession {
1383
1387
  const now = Date.now();
1384
1388
  const events = [this.thread.upsert({ id: turnId, kind: 'turn', createdAt: now, turnId, state: 'running', origin: 'user', endedAt: null, costUsd: 0 })];
1385
1389
  if (note !== null) {
1386
- events.push(this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: note }));
1390
+ events.push(this.thread.upsert(saidNote(turnId, now, note)));
1387
1391
  }
1388
1392
  if (text !== null) {
1389
1393
  const mentions = extras.mentions?.length ? extras.mentions : undefined;
@@ -9,13 +9,13 @@ export interface MessageWords {
9
9
  }
10
10
  export interface ChatNoticeTargets {
11
11
  chat(id: string): Promise<'idle' | 'running' | 'none'>;
12
- fromMessage(id: string): boolean;
12
+ wokenBy(id: string): readonly string[];
13
13
  }
14
14
  export interface ChatDelivery {
15
15
  outcome: 'wake' | 'no-chat' | 'in-turn' | 'from-message';
16
16
  waiting: number;
17
17
  }
18
- export declare function turnFromMessage(items: readonly ChatItem[], activeTurnId: string | null): boolean;
18
+ export declare function turnWokenBy(items: readonly ChatItem[], activeTurnId: string | null): readonly string[];
19
19
  export declare function chatNoticeTargets(chats: Pick<ChatCore, 'get' | 'hasStored'>): ChatNoticeTargets;
20
20
  export declare function deliverToChat(store: Pick<NoticeStore, 'put'>, targets: ChatNoticeTargets, notice: Omit<Notice, 'createdAt'>): Promise<ChatDelivery>;
21
21
  export interface NoticeChat {
@@ -1,24 +1,26 @@
1
1
  /*
2
- * Whether the turn a chat is running was itself opened by a message, which stops two agents that
3
- * read each other from waking each other forever. It rides on the turn, so it survives a restart.
4
- * Between turns the last one counts, since work it left running (a background subagent) may still
5
- * write, and a turn the CLI opened by itself takes the step of the turn before it.
2
+ * Who sent the message that opened the turn a chat is running, empty when no message opened it,
3
+ * which stops two agents that read each other from waking each other forever. It rides on the turn,
4
+ * so it survives a restart. Between turns the last one counts, since work it left running (a
5
+ * background subagent) may still write, and a turn the CLI opened by itself takes the step of the
6
+ * turn before it.
6
7
  */
7
- export function turnFromMessage(items, activeTurnId) {
8
+ export function turnWokenBy(items, activeTurnId) {
8
9
  const from = activeTurnId === null ? items.findLastIndex((item) => item.kind === 'turn') : items.findIndex((item) => item.id === activeTurnId);
9
10
  for (let i = from; i >= 0; i--) {
10
11
  const turn = items[i];
11
12
  if (turn.kind !== 'turn') {
12
13
  continue;
13
14
  }
14
- if ((turn.messageFrom ?? []).length > 0) {
15
- return true;
15
+ const senders = turn.messageFrom ?? [];
16
+ if (senders.length > 0) {
17
+ return senders;
16
18
  }
17
19
  if (turn.origin !== 'agent' || turn.taskIds !== undefined || turn.summaryFor !== undefined) {
18
- return false;
20
+ return [];
19
21
  }
20
22
  }
21
- return false;
23
+ return [];
22
24
  }
23
25
  /* The chats of a core as a message sees them. */
24
26
  export function chatNoticeTargets(chats) {
@@ -31,17 +33,19 @@ export function chatNoticeTargets(chats) {
31
33
  // A chat nobody has loaded is idle: the turn opens on the thread the host reads back from disk.
32
34
  return (await chats.hasStored(id)) ? 'idle' : 'none';
33
35
  },
34
- fromMessage: (id) => {
36
+ wokenBy: (id) => {
35
37
  const session = chats.get(id);
36
- return session !== undefined && turnFromMessage(session.thread.list(), session.info.activeTurnId);
38
+ return session === undefined ? [] : turnWokenBy(session.thread.list(), session.info.activeTurnId);
37
39
  }
38
40
  };
39
41
  }
40
42
  /*
41
43
  * One message on its way to a chat, and whether the chat owes a turn on it. A chat between turns gets
42
44
  * one, as for a settled task, or the message sits there until someone happens to prompt that chat.
43
- * One step deep: the turn a message opened wakes nobody with a message of its own. A node that runs
44
- * something else, such as a terminal, is the host's to deliver to before this.
45
+ * A turn a message opened wakes nobody with a message of its own, except the sender it answers,
46
+ * once: only while that sender's own turn was opened by no message, so a question and its answer
47
+ * take two steps and the turn the answer opens wakes nobody. A node that runs something else, such
48
+ * as a terminal, is the host's to deliver to before this.
45
49
  */
46
50
  export async function deliverToChat(store, targets, notice) {
47
51
  const waiting = await store.put(notice);
@@ -52,7 +56,9 @@ export async function deliverToChat(store, targets, notice) {
52
56
  if (chat === 'running') {
53
57
  return { outcome: 'in-turn', waiting };
54
58
  }
55
- if (targets.fromMessage(notice.from)) {
59
+ const wokenBy = targets.wokenBy(notice.from);
60
+ const answer = wokenBy.includes(notice.targetId) && targets.wokenBy(notice.targetId).length === 0;
61
+ if (wokenBy.length > 0 && !answer) {
56
62
  return { outcome: 'from-message', waiting };
57
63
  }
58
64
  return { outcome: 'wake', waiting };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents",
3
- "version": "0.48.0",
3
+ "version": "0.48.2",
4
4
  "description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -93,8 +93,8 @@
93
93
  "test": "bun --conditions=source test"
94
94
  },
95
95
  "dependencies": {
96
- "@adecore/agent-contracts": "0.48.0",
97
- "@adecore/intelligent-ui": "0.48.0",
96
+ "@adecore/agent-contracts": "0.48.2",
97
+ "@adecore/intelligent-ui": "0.48.2",
98
98
  "zod": "^4.6.7"
99
99
  },
100
100
  "devDependencies": {
@@ -6,6 +6,7 @@ import type {
6
6
  ChatGoalAction,
7
7
  ChatInfo,
8
8
  ChatItem,
9
+ ChatNoteItem,
9
10
  ChatNoteMessage,
10
11
  ChatQueuedMessage,
11
12
  ChatQuoteSource,
@@ -157,6 +158,11 @@ function newId(prefix: string): string {
157
158
  return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
158
159
  }
159
160
 
161
+ /* The note of what the agent heard in front of a prompt, which a person reads in the turn it opened. */
162
+ function saidNote(turnId: string, now: number, text: string): ChatNoteItem {
163
+ return { id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text, said: true };
164
+ }
165
+
160
166
  /*
161
167
  * Item and request ids carry the generation of the CLI that made them, and a restart of the host
162
168
  * must not hand out one a stored thread already holds. Starting from the clock puts every run above
@@ -1184,7 +1190,7 @@ export class ChatSession {
1184
1190
  const preamble = [words.preamble, context.preamble].filter(Boolean).join('\n\n') || null;
1185
1191
 
1186
1192
  if (context.note) {
1187
- this.emit([this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: context.note })]);
1193
+ this.emit([this.thread.upsert(saidNote(turnId, now, context.note))]);
1188
1194
  }
1189
1195
 
1190
1196
  const input: TurnInput = { promptId: randomUUID(), text: words.prompt, preamble, attachments: [], mentions: [], skills: [] };
@@ -1271,7 +1277,7 @@ export class ChatSession {
1271
1277
  ...(wake.note === undefined
1272
1278
  ? []
1273
1279
  : [this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: wake.note })]),
1274
- ...(note === null ? [] : [this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: note })]),
1280
+ ...(note === null ? [] : [this.thread.upsert(saidNote(turnId, now, note))]),
1275
1281
  this.thread.patchInfo({ status: this.thread.statusFor(turnId), activeTurnId: turnId })
1276
1282
  ]);
1277
1283
 
@@ -1878,7 +1884,7 @@ export class ChatSession {
1878
1884
  const events = [this.thread.upsert({ id: turnId, kind: 'turn', createdAt: now, turnId, state: 'running', origin: 'user', endedAt: null, costUsd: 0 })];
1879
1885
 
1880
1886
  if (note !== null) {
1881
- events.push(this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: note }));
1887
+ events.push(this.thread.upsert(saidNote(turnId, now, note)));
1882
1888
  }
1883
1889
 
1884
1890
  if (text !== null) {
@@ -18,8 +18,8 @@ export interface MessageWords {
18
18
  export interface ChatNoticeTargets {
19
19
  /* What the host holds under this id: a chat between turns, one that is in a turn, or no chat at all. */
20
20
  chat(id: string): Promise<'idle' | 'running' | 'none'>;
21
- /* Whether the turn this node is running was opened by a message of its own, which is where waking stops. */
22
- fromMessage(id: string): boolean;
21
+ /* The senders whose message opened the turn this node is running, empty for a turn no message opened. */
22
+ wokenBy(id: string): readonly string[];
23
23
  }
24
24
 
25
25
  /*
@@ -33,12 +33,13 @@ export interface ChatDelivery {
33
33
  }
34
34
 
35
35
  /*
36
- * Whether the turn a chat is running was itself opened by a message, which stops two agents that
37
- * read each other from waking each other forever. It rides on the turn, so it survives a restart.
38
- * Between turns the last one counts, since work it left running (a background subagent) may still
39
- * write, and a turn the CLI opened by itself takes the step of the turn before it.
36
+ * Who sent the message that opened the turn a chat is running, empty when no message opened it,
37
+ * which stops two agents that read each other from waking each other forever. It rides on the turn,
38
+ * so it survives a restart. Between turns the last one counts, since work it left running (a
39
+ * background subagent) may still write, and a turn the CLI opened by itself takes the step of the
40
+ * turn before it.
40
41
  */
41
- export function turnFromMessage(items: readonly ChatItem[], activeTurnId: string | null): boolean {
42
+ export function turnWokenBy(items: readonly ChatItem[], activeTurnId: string | null): readonly string[] {
42
43
  const from = activeTurnId === null ? items.findLastIndex((item) => item.kind === 'turn') : items.findIndex((item) => item.id === activeTurnId);
43
44
 
44
45
  for (let i = from; i >= 0; i--) {
@@ -48,16 +49,18 @@ export function turnFromMessage(items: readonly ChatItem[], activeTurnId: string
48
49
  continue;
49
50
  }
50
51
 
51
- if ((turn.messageFrom ?? []).length > 0) {
52
- return true;
52
+ const senders = turn.messageFrom ?? [];
53
+
54
+ if (senders.length > 0) {
55
+ return senders;
53
56
  }
54
57
 
55
58
  if (turn.origin !== 'agent' || turn.taskIds !== undefined || turn.summaryFor !== undefined) {
56
- return false;
59
+ return [];
57
60
  }
58
61
  }
59
62
 
60
- return false;
63
+ return [];
61
64
  }
62
65
 
63
66
  /* The chats of a core as a message sees them. */
@@ -73,10 +76,10 @@ export function chatNoticeTargets(chats: Pick<ChatCore, 'get' | 'hasStored'>): C
73
76
  // A chat nobody has loaded is idle: the turn opens on the thread the host reads back from disk.
74
77
  return (await chats.hasStored(id)) ? 'idle' : 'none';
75
78
  },
76
- fromMessage: (id) => {
79
+ wokenBy: (id) => {
77
80
  const session = chats.get(id);
78
81
 
79
- return session !== undefined && turnFromMessage(session.thread.list(), session.info.activeTurnId);
82
+ return session === undefined ? [] : turnWokenBy(session.thread.list(), session.info.activeTurnId);
80
83
  }
81
84
  };
82
85
  }
@@ -84,8 +87,10 @@ export function chatNoticeTargets(chats: Pick<ChatCore, 'get' | 'hasStored'>): C
84
87
  /*
85
88
  * One message on its way to a chat, and whether the chat owes a turn on it. A chat between turns gets
86
89
  * one, as for a settled task, or the message sits there until someone happens to prompt that chat.
87
- * One step deep: the turn a message opened wakes nobody with a message of its own. A node that runs
88
- * something else, such as a terminal, is the host's to deliver to before this.
90
+ * A turn a message opened wakes nobody with a message of its own, except the sender it answers,
91
+ * once: only while that sender's own turn was opened by no message, so a question and its answer
92
+ * take two steps and the turn the answer opens wakes nobody. A node that runs something else, such
93
+ * as a terminal, is the host's to deliver to before this.
89
94
  */
90
95
  export async function deliverToChat(store: Pick<NoticeStore, 'put'>, targets: ChatNoticeTargets, notice: Omit<Notice, 'createdAt'>): Promise<ChatDelivery> {
91
96
  const waiting = await store.put(notice);
@@ -99,7 +104,10 @@ export async function deliverToChat(store: Pick<NoticeStore, 'put'>, targets: Ch
99
104
  return { outcome: 'in-turn', waiting };
100
105
  }
101
106
 
102
- if (targets.fromMessage(notice.from)) {
107
+ const wokenBy = targets.wokenBy(notice.from);
108
+ const answer = wokenBy.includes(notice.targetId) && targets.wokenBy(notice.targetId).length === 0;
109
+
110
+ if (wokenBy.length > 0 && !answer) {
103
111
  return { outcome: 'from-message', waiting };
104
112
  }
105
113