@khorsheed/dsh-room 0.2.2 → 0.2.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.
Files changed (109) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.en.md +29 -62
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +28 -61
  5. package/lib/client.js +13097 -4182
  6. package/lib/index.js +4045 -730
  7. package/lib/journal-B4FOGO6S.js +482 -0
  8. package/lib/journal-Rid9AAnD.js +485 -0
  9. package/lib/tool.js +112 -166
  10. package/lib/typert.host.js +2031 -374
  11. package/lib/typert.remote-client.d.ts +34 -10
  12. package/lib/typert.remote-client.js +1599 -210
  13. package/lib/types/agent-setup.d.ts +1 -1
  14. package/lib/types/client/InviteAgentAction.js +4 -16
  15. package/lib/types/client/InviteDialog.d.ts +28 -6
  16. package/lib/types/client/InviteDialog.js +76 -12
  17. package/lib/types/client/MemberCard.d.ts +5 -1
  18. package/lib/types/client/MemberCard.js +4 -4
  19. package/lib/types/client/MembersView.d.ts +1 -1
  20. package/lib/types/client/MembersView.js +58 -22
  21. package/lib/types/client/RemoveMemberConfirm.d.ts +40 -0
  22. package/lib/types/client/RemoveMemberConfirm.js +85 -0
  23. package/lib/types/client/RoomApprovalView.d.ts +11 -0
  24. package/lib/types/client/RoomApprovalView.js +28 -0
  25. package/lib/types/client/RoomComposer.d.ts +1 -1
  26. package/lib/types/client/RoomComposer.js +195 -29
  27. package/lib/types/client/RoomDecisionView.d.ts +22 -0
  28. package/lib/types/client/RoomDecisionView.js +35 -0
  29. package/lib/types/client/RoomHelpView.d.ts +17 -0
  30. package/lib/types/client/RoomHelpView.js +56 -0
  31. package/lib/types/client/RoomInboxView.d.ts +10 -0
  32. package/lib/types/client/RoomInboxView.js +14 -0
  33. package/lib/types/client/RoomQuestionView.d.ts +13 -0
  34. package/lib/types/client/RoomQuestionView.js +61 -0
  35. package/lib/types/client/RoomRelayView.js +1 -1
  36. package/lib/types/client/RoomRunView.d.ts +3 -1
  37. package/lib/types/client/RoomRunView.js +12 -5
  38. package/lib/types/client/RoomSettingsView.d.ts +19 -0
  39. package/lib/types/client/RoomSettingsView.js +30 -0
  40. package/lib/types/client/RoomSpeechView.d.ts +22 -8
  41. package/lib/types/client/RoomSpeechView.js +94 -11
  42. package/lib/types/client/RoomStatsLine.d.ts +26 -5
  43. package/lib/types/client/RoomStatsLine.js +53 -7
  44. package/lib/types/client/RoomTaskCapsule.d.ts +19 -0
  45. package/lib/types/client/RoomTaskCapsule.js +37 -0
  46. package/lib/types/client/TasksTab.d.ts +41 -0
  47. package/lib/types/client/TasksTab.js +43 -0
  48. package/lib/types/client/TasksView.d.ts +44 -0
  49. package/lib/types/client/TasksView.js +149 -0
  50. package/lib/types/client/TeamView.d.ts +33 -0
  51. package/lib/types/client/TeamView.js +210 -0
  52. package/lib/types/client/at-menu.d.ts +127 -0
  53. package/lib/types/client/at-menu.js +173 -0
  54. package/lib/types/client/draft-chips.d.ts +67 -0
  55. package/lib/types/client/draft-chips.js +95 -0
  56. package/lib/types/client/executions.d.ts +0 -30
  57. package/lib/types/client/executions.js +4 -34
  58. package/lib/types/client/icons-local.d.ts +25 -0
  59. package/lib/types/client/icons-local.js +10 -0
  60. package/lib/types/client/icons.d.ts +2 -1
  61. package/lib/types/client/icons.js +1 -0
  62. package/lib/types/client/index.js +276 -72
  63. package/lib/types/client/locales.d.ts +482 -52
  64. package/lib/types/client/locales.js +490 -60
  65. package/lib/types/client/nodes.d.ts +112 -17
  66. package/lib/types/client/nodes.js +197 -26
  67. package/lib/types/client/slots.d.ts +117 -47
  68. package/lib/types/client/task-tag.d.ts +32 -0
  69. package/lib/types/client/task-tag.js +33 -0
  70. package/lib/types/client/tasks.d.ts +50 -0
  71. package/lib/types/client/tasks.js +82 -0
  72. package/lib/types/client/team.d.ts +73 -0
  73. package/lib/types/client/team.js +187 -0
  74. package/lib/types/decisions.d.ts +49 -0
  75. package/lib/types/decisions.js +101 -0
  76. package/lib/types/deliveries.d.ts +17 -1
  77. package/lib/types/deliveries.js +82 -6
  78. package/lib/types/dispatch.d.ts +92 -4
  79. package/lib/types/dispatch.js +477 -109
  80. package/lib/types/help.d.ts +137 -0
  81. package/lib/types/help.js +216 -0
  82. package/lib/types/helps.d.ts +26 -0
  83. package/lib/types/helps.js +47 -0
  84. package/lib/types/index.d.ts +292 -52
  85. package/lib/types/index.js +1415 -199
  86. package/lib/types/jobs.d.ts +22 -0
  87. package/lib/types/jobs.js +77 -0
  88. package/lib/types/journal.d.ts +17 -27
  89. package/lib/types/journal.js +120 -43
  90. package/lib/types/messaging.d.ts +65 -0
  91. package/lib/types/messaging.js +160 -0
  92. package/lib/types/plan-service.js +7 -1
  93. package/lib/types/report-gate.d.ts +81 -0
  94. package/lib/types/report-gate.js +89 -0
  95. package/lib/types/tool-definition.d.ts +43 -20
  96. package/lib/types/tool-definition.js +145 -212
  97. package/lib/types/tool.d.ts +2 -2
  98. package/lib/types/tool.js +3 -2
  99. package/lib/types/types.d.ts +755 -78
  100. package/locale/zh.json +1 -1
  101. package/package.json +3 -3
  102. package/lib/types/client/RoomActivityDock.d.ts +0 -19
  103. package/lib/types/client/RoomActivityDock.js +0 -105
  104. package/lib/types/client/RoomDockCapsules.d.ts +0 -64
  105. package/lib/types/client/RoomDockCapsules.js +0 -277
  106. package/lib/types/client/RoomPlanTab.d.ts +0 -15
  107. package/lib/types/client/RoomPlanTab.js +0 -13
  108. package/lib/types/client/RoomTaskLineView.d.ts +0 -13
  109. package/lib/types/client/RoomTaskLineView.js +0 -21
@@ -0,0 +1,187 @@
1
+ /**
2
+ * The team view's projection: who works for whom in a room, read from the
3
+ * replayed state the client already holds. A room's members are standing
4
+ * (one member takes many deliveries), so the member is the node and each
5
+ * delivery it received hangs under it; edges count what flowed between two
6
+ * members — tasks handed over, reports that came back, FYIs.
7
+ *
8
+ * The tree places every member under whoever handed it the most tasks (the
9
+ * human included), walking out from the human so the result is always a
10
+ * tree; a member nobody has tasked yet sits under the coordinator. Edges the
11
+ * tree does not draw stay available as cross links.
12
+ *
13
+ * Outside sessions the room asked for help hang under whoever asked them
14
+ * (dashed 外援 nodes, grey once every ask has ended); one kept on the member
15
+ * list (「加入房间成员」) is a solid node still marked 外部会话. Neither counts
16
+ * in the room's totals: its work and usage belong to its own session.
17
+ * @module @khorsheed/dsh-room/client/team
18
+ */
19
+ import { coordinatorMember } from '../journal.js';
20
+ /** The human, as a tree root. */
21
+ export const TEAM_ROOT = 'you';
22
+ const idOf = (member) => member.id ?? member.name;
23
+ /**
24
+ * Project the room state into the team tree.
25
+ * @param state - the replayed room state.
26
+ * @returns nodes in tree order (parents before children), edges and totals.
27
+ */
28
+ export function teamOf(state) {
29
+ const coordinator = coordinatorMember(state);
30
+ const byId = new Map(state.members.map(member => [idOf(member), member]));
31
+ const nameOf = (id) => id === TEAM_ROOT ? TEAM_ROOT : byId.get(id)?.name ?? id;
32
+ const edges = new Map();
33
+ const edge = (from, to) => {
34
+ const key = `${from}\u0000${to}`;
35
+ let row = edges.get(key);
36
+ if (row === undefined)
37
+ edges.set(key, row = { from, to, tasks: 0, reports: 0, notifies: 0 });
38
+ return row;
39
+ };
40
+ const received = new Map();
41
+ for (const delivery of state.deliveries ?? []) {
42
+ if (!byId.has(delivery.memberId) || delivery.origin === 'relay')
43
+ continue;
44
+ const from = delivery.origin === 'human' ? TEAM_ROOT
45
+ : delivery.origin === 'report' ? undefined
46
+ : delivery.from ?? (delivery.origin === 'coordinator' && coordinator !== undefined ? idOf(coordinator) : undefined);
47
+ const kind = delivery.origin === 'report' ? 'report' : delivery.kind === 'notify' ? 'notify' : 'task';
48
+ if (from !== undefined && from !== delivery.memberId) {
49
+ const row = edge(from, delivery.memberId);
50
+ if (kind === 'notify')
51
+ row.notifies++;
52
+ else {
53
+ row.tasks++;
54
+ if (delivery.replyTo !== undefined && ['done', 'failed', 'cancelled', 'uncertain'].includes(delivery.status))
55
+ row.reports++;
56
+ }
57
+ }
58
+ const list = received.get(delivery.memberId) ?? [];
59
+ list.push({ id: delivery.id, seq: delivery.dispatchSeq, from: from === undefined ? '' : nameOf(from), text: delivery.text, status: delivery.status, kind });
60
+ received.set(delivery.memberId, list);
61
+ }
62
+ // Parent = the source that handed this member the most tasks, placed by a
63
+ // walk from the human so no cycle can form.
64
+ const best = new Map();
65
+ for (const member of state.members) {
66
+ const id = idOf(member);
67
+ if (coordinator !== undefined && id === idOf(coordinator))
68
+ continue;
69
+ const top = [...edges.values()].filter(row => row.to === id && row.tasks > 0).sort((a, b) => b.tasks - a.tasks)[0];
70
+ if (top !== undefined)
71
+ best.set(id, top.from);
72
+ }
73
+ const parent = new Map();
74
+ const order = [];
75
+ const queue = [TEAM_ROOT];
76
+ if (coordinator !== undefined) {
77
+ parent.set(idOf(coordinator), TEAM_ROOT);
78
+ }
79
+ while (queue.length > 0) {
80
+ const at = queue.shift();
81
+ const children = at === TEAM_ROOT && coordinator !== undefined
82
+ ? [idOf(coordinator), ...state.members.map(idOf).filter(id => best.get(id) === TEAM_ROOT)]
83
+ : state.members.map(idOf).filter(id => best.get(id) === at);
84
+ for (const child of children) {
85
+ if (order.includes(child))
86
+ continue;
87
+ parent.set(child, at);
88
+ order.push(child);
89
+ queue.push(child);
90
+ }
91
+ }
92
+ for (const member of state.members) {
93
+ const id = idOf(member);
94
+ if (order.includes(id))
95
+ continue;
96
+ parent.set(id, coordinator !== undefined && idOf(coordinator) !== id ? idOf(coordinator) : TEAM_ROOT);
97
+ order.push(id);
98
+ }
99
+ const memberNodes = order.map((id) => {
100
+ const member = byId.get(id);
101
+ const run = state.runs.find(entry => entry.member === member.name);
102
+ const executions = (state.executions ?? []).filter(row => row.memberId === id);
103
+ const current = run?.state === 'running'
104
+ ? (state.deliveries ?? []).find(row => row.memberId === id && row.status === 'running' && (run.runId === undefined || row.id === run.runId))?.text.trim().split('\n', 1)[0]
105
+ : undefined;
106
+ const model = executions.findLast(row => row.model !== undefined)?.model ?? member.model;
107
+ return {
108
+ id, member, parent: parent.get(id) ?? TEAM_ROOT,
109
+ coordinator: coordinator !== undefined && idOf(coordinator) === id,
110
+ status: run?.state === 'running' ? 'running' : run?.state === 'failed' ? 'failed' : 'idle',
111
+ ...current === undefined ? {} : { current },
112
+ ...model === undefined ? {} : { model },
113
+ tokens: executions.reduce((sum, row) => sum + (row.tokens ?? 0), 0),
114
+ elapsedMs: executions.reduce((sum, row) => sum + (row.elapsedMs ?? 0), 0),
115
+ rounds: executions.length,
116
+ deliveries: (received.get(id) ?? []).slice().reverse(),
117
+ };
118
+ });
119
+ const nodes = [...memberNodes, ...externalNodes(state, memberNodes)];
120
+ const own = nodes.filter(node => node.external === undefined);
121
+ return {
122
+ nodes,
123
+ edges: [...edges.values()],
124
+ totals: {
125
+ members: own.length,
126
+ running: own.filter(node => node.status === 'running').length,
127
+ deliveries: (state.deliveries ?? []).filter(row => row.origin !== 'report' && row.origin !== 'relay').length,
128
+ tokens: own.reduce((sum, node) => sum + node.tokens, 0),
129
+ elapsedMs: own.reduce((sum, node) => sum + node.elapsedMs, 0),
130
+ },
131
+ };
132
+ }
133
+ /** The id of an outside session's node. */
134
+ export const externalId = (sessionId, member) => `ext:${sessionId}${member === undefined ? '' : `:${member}`}`;
135
+ /** A help thread's status as a delivery outcome. */
136
+ function helpStatus(help) {
137
+ return help.state === 'replied' ? 'done' : help.state === 'failed' ? 'failed' : help.state === 'queued' ? 'queued' : 'running';
138
+ }
139
+ /**
140
+ * The outside sessions' nodes: one per asked target (a room's member counts
141
+ * as its own target), under whoever asked it last, plus every linked session
142
+ * nobody has asked yet, under the coordinator.
143
+ */
144
+ function externalNodes(state, members) {
145
+ const coordinator = members.find(node => node.coordinator);
146
+ const linked = new Map((state.linked ?? []).map(entry => [entry.sessionId, entry]));
147
+ const groups = new Map();
148
+ for (const help of state.helps ?? []) {
149
+ const key = externalId(help.target.sessionId, help.target.member);
150
+ groups.set(key, [...groups.get(key) ?? [], help]);
151
+ }
152
+ const nodes = [];
153
+ for (const [id, helps] of groups) {
154
+ const last = helps.at(-1);
155
+ const target = last.target;
156
+ const asker = members.find(node => node.id === last.from);
157
+ const title = target.member === undefined ? target.title : `${target.member}(${target.title})`;
158
+ const done = helps.every(help => help.state === 'replied' || help.state === 'failed');
159
+ nodes.push({
160
+ id, parent: asker?.id ?? coordinator?.id ?? TEAM_ROOT, coordinator: false,
161
+ member: { name: title, kind: 'cli', invitedBy: 'agent', childSessionId: target.sessionId },
162
+ status: done ? 'idle' : helps.some(help => help.state === 'running' || help.state === 'asking') ? 'running' : 'idle',
163
+ tokens: 0, elapsedMs: 0, rounds: helps.length,
164
+ deliveries: helps.slice().reverse().map(help => ({ id: help.id, seq: 0, from: help.fromName, text: help.text, status: helpStatus(help), kind: 'task' })),
165
+ external: { sessionId: target.sessionId, linked: target.member === undefined && linked.has(target.sessionId), room: target.room === true, done, helps },
166
+ });
167
+ }
168
+ for (const entry of linked.values()) {
169
+ if (groups.has(externalId(entry.sessionId)))
170
+ continue;
171
+ nodes.push({
172
+ id: externalId(entry.sessionId), parent: coordinator?.id ?? TEAM_ROOT, coordinator: false,
173
+ member: { name: entry.title, kind: 'cli', invitedBy: 'human', childSessionId: entry.sessionId },
174
+ status: 'idle', tokens: 0, elapsedMs: 0, rounds: 0, deliveries: [],
175
+ external: { sessionId: entry.sessionId, linked: true, room: false, done: false, helps: [] },
176
+ });
177
+ }
178
+ return nodes;
179
+ }
180
+ /** Compact token count: 1.2M / 288k / 512. */
181
+ export function formatTokens(tokens) {
182
+ if (tokens >= 1_000_000)
183
+ return `${(tokens / 1_000_000).toFixed(tokens >= 10_000_000 ? 0 : 1)}M`;
184
+ if (tokens >= 1000)
185
+ return `${Math.round(tokens / 1000)}k`;
186
+ return String(tokens);
187
+ }
@@ -0,0 +1,49 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { RoomDecisionCall, RoomDecisionEvent, RoomDecisionScenario } from './types.ts';
3
+ /** The bounded decision-model call. */
4
+ export declare const DECISION_TIMEOUT_MS = 8000;
5
+ /** One fixed question of a scenario (the typesafe wire shape). */
6
+ export interface DecisionQuestion {
7
+ readonly id: string;
8
+ readonly type: 'noul' | 'choice';
9
+ readonly instructions: string;
10
+ readonly criteria?: Record<string, string>;
11
+ }
12
+ /** The scenario table: each scenario's fixed question (its criteria, when they depend on the room, are filled per call). */
13
+ export declare const DECISION_SCENARIOS: Record<RoomDecisionScenario, {
14
+ readonly question: DecisionQuestion;
15
+ }>;
16
+ /** A model answer to one question, with the call that produced it. */
17
+ export type DecisionAnswer = {
18
+ readonly ok: true;
19
+ readonly answer: unknown;
20
+ readonly probabilities?: Record<string, number>;
21
+ readonly call: RoomDecisionCall;
22
+ } | {
23
+ readonly ok: false;
24
+ readonly call?: RoomDecisionCall;
25
+ };
26
+ /**
27
+ * Whether the decision model is mounted.
28
+ * @param ctx - host context.
29
+ * @returns true when `typesafe.judge` can be called.
30
+ */
31
+ export declare function hasDecisionModel(ctx: Context): boolean;
32
+ /**
33
+ * Ask the decision model one scenario question about a state, recording the
34
+ * call whatever happens. Never throws: no service, a refusal, a timeout or a
35
+ * malformed answer all come back as `ok: false` (with the call when one went out).
36
+ * @param ctx - host context (probes `typesafe`).
37
+ * @param question - the scenario's question, criteria filled.
38
+ * @param state - the JSON-safe state the model judges.
39
+ * @returns the answer and the recorded call.
40
+ */
41
+ export declare function askDecisionModel(ctx: Context, question: DecisionQuestion, state: unknown): Promise<DecisionAnswer>;
42
+ /**
43
+ * Build one decision record.
44
+ * @param scenario - the scenario.
45
+ * @param fields - outcome, rule or probability, the call, what it was about.
46
+ * @returns the `room/decision` event data.
47
+ */
48
+ export declare function decisionRecord(scenario: RoomDecisionScenario, fields: Omit<RoomDecisionEvent, 'id' | 'scenario'>): RoomDecisionEvent;
49
+ //# sourceMappingURL=decisions.d.ts.map
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The room's own decisions — one table entry per scenario. A scenario checks
3
+ * its rules in order (the first that applies decides) and only then asks the
4
+ * decision model (the optional `typesafe` service, probed — room never
5
+ * depends on it) one fixed question about a state the room assembles from
6
+ * its log. Every decision becomes a `room/decision` record: the rule that
7
+ * decided, or the model call as it went out and came back (state, question,
8
+ * answer, model, tokens, latency, cache, failure). Settings, the timeline row
9
+ * and Context Lens all read the same scenario ids.
10
+ *
11
+ * Today's scenarios:
12
+ * - `task-filing`: does an untagged human message continue an open task, or
13
+ * start a new one?
14
+ * - `report-timing`: is an arrived member result handed to the member waiting
15
+ * on it now, or held to be handed over together later?
16
+ * @module @khorsheed/dsh-room/decisions
17
+ */
18
+ import { randomUUID } from 'node:crypto';
19
+ /** The bounded decision-model call. */
20
+ export const DECISION_TIMEOUT_MS = 8000;
21
+ /** The scenario table: each scenario's fixed question (its criteria, when they depend on the room, are filled per call). */
22
+ export const DECISION_SCENARIOS = {
23
+ 'task-filing': {
24
+ question: {
25
+ id: 'task',
26
+ type: 'choice',
27
+ instructions: 'A human sent this message to a team of agents that is still working on the open tasks listed. Does the message add to, correct or ask about one of those tasks, or does it start a new one?',
28
+ },
29
+ },
30
+ 'report-timing': {
31
+ question: {
32
+ id: 'deliver_now',
33
+ type: 'noul',
34
+ instructions: 'A coordinating agent delegated work to other agents and their results arrive one by one. The human can already '
35
+ + 'read every result in the room. Should the results that have arrived be handed to the coordinator now? Answer '
36
+ + 'yes when they let it move the work forward (start the next step, answer the human, react to a problem) or when '
37
+ + 'the human has been waiting long enough that an update helps. Answer no when the coordinator said it is waiting '
38
+ + 'for the remaining results and those are still running normally.',
39
+ },
40
+ },
41
+ };
42
+ /** A JSON-safe copy (the record must survive the session log). */
43
+ function json(value) {
44
+ return JSON.parse(JSON.stringify(value ?? null));
45
+ }
46
+ /**
47
+ * Whether the decision model is mounted.
48
+ * @param ctx - host context.
49
+ * @returns true when `typesafe.judge` can be called.
50
+ */
51
+ export function hasDecisionModel(ctx) {
52
+ return typeof ctx.get('typesafe')?.judge === 'function';
53
+ }
54
+ /**
55
+ * Ask the decision model one scenario question about a state, recording the
56
+ * call whatever happens. Never throws: no service, a refusal, a timeout or a
57
+ * malformed answer all come back as `ok: false` (with the call when one went out).
58
+ * @param ctx - host context (probes `typesafe`).
59
+ * @param question - the scenario's question, criteria filled.
60
+ * @param state - the JSON-safe state the model judges.
61
+ * @returns the answer and the recorded call.
62
+ */
63
+ export async function askDecisionModel(ctx, question, state) {
64
+ const service = ctx.get('typesafe');
65
+ if (typeof service?.judge !== 'function')
66
+ return { ok: false };
67
+ const request = { state: json(state), questions: json([question]) };
68
+ const asked = service.config?.defaultModel ?? 'typesafe';
69
+ const started = Date.now();
70
+ try {
71
+ const result = await service.judge({ state, questions: [question] }, { timeoutMs: DECISION_TIMEOUT_MS, fresh: true });
72
+ if (!result.ok) {
73
+ return { ok: false, call: { model: asked, latencyMs: Date.now() - started, request, failure: { reason: result.reason, ...result.detail === undefined ? {} : { detail: result.detail } } } };
74
+ }
75
+ const judgement = result.judgement;
76
+ // Only what the service reported: an absent field stays absent (the log takes JSON only).
77
+ const call = {
78
+ model: typeof judgement.model === 'string' && judgement.model !== '' ? judgement.model : asked,
79
+ latencyMs: typeof judgement.latencyMs === 'number' ? judgement.latencyMs : Date.now() - started,
80
+ ...typeof judgement.cached === 'boolean' ? { cached: judgement.cached } : {},
81
+ request, response: json(judgement.decisions),
82
+ ...judgement.usage === undefined ? {} : { inputTokens: judgement.usage.inputTokens, outputTokens: judgement.usage.outputTokens },
83
+ };
84
+ const decision = judgement.decisions.find(entry => entry.id === question.id);
85
+ if (decision === undefined)
86
+ return { ok: false, call: { ...call, failure: { reason: 'decode', detail: 'no answer to the question' } } };
87
+ return { ok: true, answer: decision.answer, ...decision.probabilities === undefined ? {} : { probabilities: decision.probabilities }, call };
88
+ }
89
+ catch (error) {
90
+ return { ok: false, call: { model: asked, latencyMs: Date.now() - started, request, failure: { reason: 'transport', detail: String(error) } } };
91
+ }
92
+ }
93
+ /**
94
+ * Build one decision record.
95
+ * @param scenario - the scenario.
96
+ * @param fields - outcome, rule or probability, the call, what it was about.
97
+ * @returns the `room/decision` event data.
98
+ */
99
+ export function decisionRecord(scenario, fields) {
100
+ return { id: randomUUID(), scenario, ...fields };
101
+ }
@@ -1,5 +1,21 @@
1
1
  import type { SessionEvent } from '@deepseek-ai/dsh-session/types';
2
2
  import type { RoomDelivery } from './types.ts';
3
- /** Accepted delivery intents and their latest durable outcomes. */
3
+ /** Whether an event is a message the human typed (not a producer's followup); an in-place edit of one (message-tools) is the human's text too. */
4
+ export declare function isHumanMessage(event: SessionEvent): event is SessionEvent<'user/message'>;
5
+ /** Every human message's latest task link (message id → task root id). */
6
+ export declare function taskLinks(events: readonly SessionEvent[]): Map<string, string>;
7
+ /** The task a human message belongs to: its latest link, else its own id (a task of its own). */
8
+ export declare function linkedTask(links: ReadonlyMap<string, string>, messageId: string): string;
9
+ /**
10
+ * Accepted delivery intents and their latest durable outcomes, with the
11
+ * timing edges the task views draw (dispatched, admitted, settled, held,
12
+ * released). A delivery's task is its dispatch's `task`; dispatches written
13
+ * before tasks were recorded fall back by cause: a report or release takes
14
+ * its source's task, anything else the task of the latest human message
15
+ * before it (the turn that sent it), and a human dispatch with no message
16
+ * (an invite's first task) is a task of its own.
17
+ * @param events - the room log.
18
+ * @returns the deliveries in dispatch order.
19
+ */
4
20
  export declare function replayDeliveries(events: readonly SessionEvent[]): RoomDelivery[];
5
21
  //# sourceMappingURL=deliveries.d.ts.map
@@ -1,19 +1,95 @@
1
- /** Accepted delivery intents and their latest durable outcomes. */
1
+ /** Whether an event is a message the human typed (not a producer's followup); an in-place edit of one (message-tools) is the human's text too. */
2
+ export function isHumanMessage(event) {
3
+ if (event.type !== 'user/message')
4
+ return false;
5
+ const source = event.data.source;
6
+ return source.kind === 'user' || (source.kind === 'message-tools' && source.op === 'edit');
7
+ }
8
+ /** Every human message's latest task link (message id → task root id). */
9
+ export function taskLinks(events) {
10
+ const links = new Map();
11
+ for (const event of events)
12
+ if (event.type === 'room/task-link')
13
+ links.set(event.data.message, event.data.task);
14
+ return links;
15
+ }
16
+ /** The task a human message belongs to: its latest link, else its own id (a task of its own). */
17
+ export function linkedTask(links, messageId) {
18
+ return links.get(messageId) ?? messageId;
19
+ }
20
+ /**
21
+ * Accepted delivery intents and their latest durable outcomes, with the
22
+ * timing edges the task views draw (dispatched, admitted, settled, held,
23
+ * released). A delivery's task is its dispatch's `task`; dispatches written
24
+ * before tasks were recorded fall back by cause: a report or release takes
25
+ * its source's task, anything else the task of the latest human message
26
+ * before it (the turn that sent it), and a human dispatch with no message
27
+ * (an invite's first task) is a task of its own.
28
+ * @param events - the room log.
29
+ * @returns the deliveries in dispatch order.
30
+ */
2
31
  export function replayDeliveries(events) {
3
32
  const rows = new Map();
33
+ const taskBySeq = new Map();
34
+ const links = new Map();
35
+ let lastHuman;
36
+ const set = (id, patch) => {
37
+ const row = rows.get(id);
38
+ if (row !== undefined)
39
+ rows.set(id, { ...row, ...patch });
40
+ };
4
41
  for (const event of events) {
5
- if (event.type === 'room/dispatch' && event.data.targetIds !== undefined) {
6
- for (const memberId of event.data.targetIds) {
42
+ if (isHumanMessage(event)) {
43
+ lastHuman = { id: event.data.id, seq: event.seq };
44
+ }
45
+ else if (event.type === 'room/task-link') {
46
+ links.set(event.data.message, event.data.task);
47
+ }
48
+ else if (event.type === 'room/dispatch' && event.data.targetIds !== undefined) {
49
+ const data = event.data;
50
+ const targetIds = data.targetIds ?? [];
51
+ const sourceSeq = data.reportFor !== undefined && /^\d+:/.test(data.reportFor) ? Number(data.reportFor.split(':', 1)[0]) : data.releases?.[0];
52
+ const task = data.task
53
+ ?? (sourceSeq === undefined ? undefined : taskBySeq.get(sourceSeq))
54
+ ?? (data.origin === 'human' && lastHuman === undefined ? data.id : undefined)
55
+ ?? (lastHuman === undefined ? data.id : linkedTask(links, lastHuman.id));
56
+ if (task !== undefined)
57
+ taskBySeq.set(event.seq, task);
58
+ for (const memberId of targetIds) {
7
59
  const id = `${event.seq}:${memberId}`;
8
- if (!rows.has(id))
9
- rows.set(id, { id, dispatchSeq: event.seq, memberId, status: 'queued', text: event.data.text, origin: event.data.origin ?? 'human', ...event.data.plan === undefined ? {} : { plan: event.data.plan } });
60
+ if (rows.has(id))
61
+ continue;
62
+ rows.set(id, {
63
+ id, dispatchSeq: event.seq, memberId, status: data.held === true ? 'held' : 'queued', text: data.text,
64
+ origin: data.origin ?? 'human', queuedAt: event.time,
65
+ ...data.replyTo === undefined ? {} : { replyTo: data.replyTo },
66
+ ...data.from === undefined ? {} : { from: data.from },
67
+ ...data.kind === undefined ? {} : { kind: data.kind },
68
+ ...data.plan === undefined ? {} : { plan: data.plan },
69
+ ...task === undefined ? {} : { task },
70
+ ...data.parent === undefined ? {} : { parent: data.parent },
71
+ ...data.held === true ? { heldAt: event.time } : {},
72
+ });
10
73
  }
11
74
  }
12
75
  else if (event.type === 'room/delivery-state') {
13
76
  const row = rows.get(event.data.id);
14
77
  if (row === undefined || row.memberId !== event.data.memberId || row.dispatchSeq !== event.data.dispatchSeq)
15
78
  continue;
16
- rows.set(row.id, { ...row, status: event.data.state, ...event.data.error === undefined ? {} : { error: event.data.error } });
79
+ const terminal = event.data.state !== 'running';
80
+ rows.set(row.id, {
81
+ ...row, status: event.data.state,
82
+ ...event.data.error === undefined ? {} : { error: event.data.error },
83
+ ...event.data.state === 'running' && row.startedAt === undefined ? { startedAt: event.time } : {},
84
+ ...terminal ? { endedAt: event.time } : {},
85
+ });
86
+ }
87
+ else if (event.type === 'room/report-gate') {
88
+ const id = `${event.data.dispatchSeq}:${event.data.memberId}`;
89
+ if (event.data.verdict === 'hold')
90
+ set(id, { heldAt: event.time });
91
+ else if (event.data.verdict === 'release' && rows.get(id)?.releasedAt === undefined)
92
+ set(id, { releasedAt: event.time });
17
93
  }
18
94
  }
19
95
  return [...rows.values()];
@@ -1,7 +1,8 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
- import type { Session } from '@deepseek-ai/dsh-session';
2
+ import { SessionId, type Session } from '@deepseek-ai/dsh-session';
3
+ import type { SessionEvent } from '@deepseek-ai/dsh-session/types';
3
4
  import { type ContextFormed, type MessageSource } from '@deepseek-ai/dsh-llm';
4
- import type { RoomState } from './types.ts';
5
+ import type { RoomMember, RoomState } from './types.ts';
5
6
  /**
6
7
  * The room producer's durable identity in its two historical forms: the
7
8
  * released V3 wrapper's `plugin` value (`@khorsheed/dsh-room` — the official
@@ -22,6 +23,27 @@ declare module '@deepseek-ai/dsh-llm' {
22
23
  }
23
24
  /** The producer-owned source of a room main-agent followup. */
24
25
  export declare function roomSource(): MessageSource;
26
+ /**
27
+ * A run-state edge's child-session identity: the main agent runs on the room
28
+ * itself, external members on their child session. Optional — a fresh CLI
29
+ * member's early running edge predates the child handle's publication (the
30
+ * terminal edge and the journal backfill cover it).
31
+ */
32
+ export declare function runChildSessionId(roomId: SessionId, member: RoomMember | undefined): SessionId | undefined;
33
+ /**
34
+ * The child session round a run reached: the last turn/start the child wrote.
35
+ * Live sessions only — a reconcile path whose child is not in memory omits it.
36
+ */
37
+ /**
38
+ * The id of the last assistant message a session's log holds after `afterSeq`:
39
+ * the message a member's round closed with, which per-message tools (Context
40
+ * Lens) address in the member's own session.
41
+ * @param session - the member's session.
42
+ * @param afterSeq - only messages after this seq count.
43
+ * @returns the message id, or undefined when the round produced none.
44
+ */
45
+ export declare function lastAssistantMessageId(session: Session | undefined, afterSeq?: number): string | undefined;
46
+ export declare function runChildTurn(session: Session | undefined): number | undefined;
25
47
  /**
26
48
  * Whether a durable source belongs to the room producer, in every form the
27
49
  * log can hold: the current producer-owned kind (`room`), the V3→V4 migrated
@@ -43,6 +65,20 @@ export interface DispatchOptions {
43
65
  /** Relays this dispatch DELIVERS (confirmRelay's own payloads). */
44
66
  readonly relayIds?: readonly string[];
45
67
  }
68
+ /**
69
+ * Reports held for a member that it has not yet seen in a round: hold
70
+ * verdicts without a later release.
71
+ */
72
+ export declare function pendingHolds(events: readonly SessionEvent[], recipientId: string): readonly number[];
73
+ /**
74
+ * The held reports of a member as one prompt section (their envelopes and
75
+ * texts), or undefined when nothing is held — what a turn the room did not
76
+ * start (a human waking the agent directly) must still carry.
77
+ */
78
+ export declare function heldReportsText(events: readonly SessionEvent[], recipientId: string): {
79
+ readonly text: string;
80
+ readonly seqs: readonly number[];
81
+ } | undefined;
46
82
  export interface RunOutcome {
47
83
  readonly state: 'done' | 'cancelled' | 'failed';
48
84
  readonly text?: string;
@@ -65,6 +101,16 @@ export declare class DispatchEngine {
65
101
  private readonly queues;
66
102
  private readonly scheduled;
67
103
  private readonly inFlight;
104
+ /** room + recipient → the wake-anyway timer for its held reports. */
105
+ private readonly holdTimers;
106
+ /**
107
+ * room id → member ids whose FIRST start is in flight: the member has no
108
+ * child session id in the journal yet, but its process may already be
109
+ * asking the host for its mode tools (claude lists MCP tools at spawn).
110
+ */
111
+ private readonly freshStarts;
112
+ /** Members of this room whose first start is in flight (see {@link freshStarts}). */
113
+ startingMembers(roomId: string): readonly string[];
68
114
  /**
69
115
  * @param ctx - host context carrying the session store and agents registry.
70
116
  */
@@ -87,19 +133,61 @@ export declare class DispatchEngine {
87
133
  recover(room: Session): Promise<void>;
88
134
  /** Execute one dispatch: run-state running → member turn → settle edges. */
89
135
  private run;
90
- private report;
136
+ /**
137
+ * Report one delivery's outcome to whoever is waiting on it (the
138
+ * coordinator for plan work, else the dispatch's `replyTo`), at most once
139
+ * per delivery. Public so a removal can settle the waits it breaks.
140
+ */
141
+ reportOutcome(room: Session, dispatchSeq: number, deliveryId: string, fromId: string, outcome: string, text: string): Promise<void>;
91
142
  /**
92
143
  * Mark the relays this run delivered as sent. Delivery = the member's own
93
144
  * session received the prompt (the followup accepted / the CLI run
94
145
  * started): the text lives in the member's private context from then on,
95
146
  * so a later run failure cannot un-deliver it.
96
147
  */
148
+ /**
149
+ * A round of this member started: every report held for it is now in its
150
+ * context — carried by this prompt (`seqs`) or already in its inbox — so
151
+ * close those holds and drop the member's wake timer.
152
+ */
153
+ markHeldReleased(room: Session, member: RoomMember, seqs: readonly number[]): Promise<void>;
154
+ private holdKey;
155
+ private clearHoldTimer;
156
+ /**
157
+ * Keep a report for later: journal it as a held dispatch and arm the
158
+ * wake-anyway timer. It is NOT pushed into an agent inbox: the host runs
159
+ * one turn per queued inbox message, so held reports would each wake their
160
+ * own turn. They ride the recipient's next round instead — the room's own
161
+ * prompt, or (a human waking a native agent directly) the turn's first step
162
+ * through {@link heldReportsMessage}.
163
+ */
164
+ private holdReport;
165
+ /**
166
+ * Wake a member for the reports held for it (the hold timeout, a human's
167
+ * 「现在交给」, or a restart). One short release round: an inbox already
168
+ * holds the reports; a CLI member gets them carried by the round's prompt.
169
+ * A no-op when nothing is held any more.
170
+ * @returns whether a release round was dispatched.
171
+ */
172
+ releaseHeld(room: Session, recipientId: string, reason: string): Promise<boolean>;
97
173
  private markSent;
98
174
  /** Main-agent member turn: a producer-sourced followup on the room's own agent. */
99
175
  private runMainAgent;
176
+ /**
177
+ * The native member's agent: live, resumed from its session after a
178
+ * restart, or created on its first round — a child session + agent in this
179
+ * host process (owned by the room's agent when live), composing the
180
+ * member's preset at creation. The handle is journaled before any prompt.
181
+ */
182
+ private nativeAgent;
183
+ /**
184
+ * Native member turn: a room-sourced followup on the member's own agent,
185
+ * mirrored into the room as speech like a CLI member's reply.
186
+ */
187
+ private runNativeMember;
100
188
  /** CLI member turn: facade start (first round) or resume, then the speech mirror. */
101
189
  private runCliMember;
102
- /** Settle only this execution and its own task rows, preserving newer runs. */
190
+ /** Settle only this execution, preserving newer runs. */
103
191
  private settle;
104
192
  }
105
193
  //# sourceMappingURL=dispatch.d.ts.map