@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.
- package/CHANGELOG.md +53 -0
- package/README.en.md +29 -62
- package/README.i18n.yaml +2 -2
- package/README.md +28 -61
- package/lib/client.js +13097 -4182
- package/lib/index.js +4045 -730
- package/lib/journal-B4FOGO6S.js +482 -0
- package/lib/journal-Rid9AAnD.js +485 -0
- package/lib/tool.js +112 -166
- package/lib/typert.host.js +2031 -374
- package/lib/typert.remote-client.d.ts +34 -10
- package/lib/typert.remote-client.js +1599 -210
- package/lib/types/agent-setup.d.ts +1 -1
- package/lib/types/client/InviteAgentAction.js +4 -16
- package/lib/types/client/InviteDialog.d.ts +28 -6
- package/lib/types/client/InviteDialog.js +76 -12
- package/lib/types/client/MemberCard.d.ts +5 -1
- package/lib/types/client/MemberCard.js +4 -4
- package/lib/types/client/MembersView.d.ts +1 -1
- package/lib/types/client/MembersView.js +58 -22
- package/lib/types/client/RemoveMemberConfirm.d.ts +40 -0
- package/lib/types/client/RemoveMemberConfirm.js +85 -0
- package/lib/types/client/RoomApprovalView.d.ts +11 -0
- package/lib/types/client/RoomApprovalView.js +28 -0
- package/lib/types/client/RoomComposer.d.ts +1 -1
- package/lib/types/client/RoomComposer.js +195 -29
- package/lib/types/client/RoomDecisionView.d.ts +22 -0
- package/lib/types/client/RoomDecisionView.js +35 -0
- package/lib/types/client/RoomHelpView.d.ts +17 -0
- package/lib/types/client/RoomHelpView.js +56 -0
- package/lib/types/client/RoomInboxView.d.ts +10 -0
- package/lib/types/client/RoomInboxView.js +14 -0
- package/lib/types/client/RoomQuestionView.d.ts +13 -0
- package/lib/types/client/RoomQuestionView.js +61 -0
- package/lib/types/client/RoomRelayView.js +1 -1
- package/lib/types/client/RoomRunView.d.ts +3 -1
- package/lib/types/client/RoomRunView.js +12 -5
- package/lib/types/client/RoomSettingsView.d.ts +19 -0
- package/lib/types/client/RoomSettingsView.js +30 -0
- package/lib/types/client/RoomSpeechView.d.ts +22 -8
- package/lib/types/client/RoomSpeechView.js +94 -11
- package/lib/types/client/RoomStatsLine.d.ts +26 -5
- package/lib/types/client/RoomStatsLine.js +53 -7
- package/lib/types/client/RoomTaskCapsule.d.ts +19 -0
- package/lib/types/client/RoomTaskCapsule.js +37 -0
- package/lib/types/client/TasksTab.d.ts +41 -0
- package/lib/types/client/TasksTab.js +43 -0
- package/lib/types/client/TasksView.d.ts +44 -0
- package/lib/types/client/TasksView.js +149 -0
- package/lib/types/client/TeamView.d.ts +33 -0
- package/lib/types/client/TeamView.js +210 -0
- package/lib/types/client/at-menu.d.ts +127 -0
- package/lib/types/client/at-menu.js +173 -0
- package/lib/types/client/draft-chips.d.ts +67 -0
- package/lib/types/client/draft-chips.js +95 -0
- package/lib/types/client/executions.d.ts +0 -30
- package/lib/types/client/executions.js +4 -34
- package/lib/types/client/icons-local.d.ts +25 -0
- package/lib/types/client/icons-local.js +10 -0
- package/lib/types/client/icons.d.ts +2 -1
- package/lib/types/client/icons.js +1 -0
- package/lib/types/client/index.js +276 -72
- package/lib/types/client/locales.d.ts +482 -52
- package/lib/types/client/locales.js +490 -60
- package/lib/types/client/nodes.d.ts +112 -17
- package/lib/types/client/nodes.js +197 -26
- package/lib/types/client/slots.d.ts +117 -47
- package/lib/types/client/task-tag.d.ts +32 -0
- package/lib/types/client/task-tag.js +33 -0
- package/lib/types/client/tasks.d.ts +50 -0
- package/lib/types/client/tasks.js +82 -0
- package/lib/types/client/team.d.ts +73 -0
- package/lib/types/client/team.js +187 -0
- package/lib/types/decisions.d.ts +49 -0
- package/lib/types/decisions.js +101 -0
- package/lib/types/deliveries.d.ts +17 -1
- package/lib/types/deliveries.js +82 -6
- package/lib/types/dispatch.d.ts +92 -4
- package/lib/types/dispatch.js +477 -109
- package/lib/types/help.d.ts +137 -0
- package/lib/types/help.js +216 -0
- package/lib/types/helps.d.ts +26 -0
- package/lib/types/helps.js +47 -0
- package/lib/types/index.d.ts +292 -52
- package/lib/types/index.js +1415 -199
- package/lib/types/jobs.d.ts +22 -0
- package/lib/types/jobs.js +77 -0
- package/lib/types/journal.d.ts +17 -27
- package/lib/types/journal.js +120 -43
- package/lib/types/messaging.d.ts +65 -0
- package/lib/types/messaging.js +160 -0
- package/lib/types/plan-service.js +7 -1
- package/lib/types/report-gate.d.ts +81 -0
- package/lib/types/report-gate.js +89 -0
- package/lib/types/tool-definition.d.ts +43 -20
- package/lib/types/tool-definition.js +145 -212
- package/lib/types/tool.d.ts +2 -2
- package/lib/types/tool.js +3 -2
- package/lib/types/types.d.ts +755 -78
- package/locale/zh.json +1 -1
- package/package.json +3 -3
- package/lib/types/client/RoomActivityDock.d.ts +0 -19
- package/lib/types/client/RoomActivityDock.js +0 -105
- package/lib/types/client/RoomDockCapsules.d.ts +0 -64
- package/lib/types/client/RoomDockCapsules.js +0 -277
- package/lib/types/client/RoomPlanTab.d.ts +0 -15
- package/lib/types/client/RoomPlanTab.js +0 -13
- package/lib/types/client/RoomTaskLineView.d.ts +0 -13
- 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
|
-
/**
|
|
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
|
package/lib/types/deliveries.js
CHANGED
|
@@ -1,19 +1,95 @@
|
|
|
1
|
-
/**
|
|
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
|
|
6
|
-
|
|
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 (
|
|
9
|
-
|
|
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
|
-
|
|
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()];
|
package/lib/types/dispatch.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
-
import type
|
|
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
|
-
|
|
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
|
|
190
|
+
/** Settle only this execution, preserving newer runs. */
|
|
103
191
|
private settle;
|
|
104
192
|
}
|
|
105
193
|
//# sourceMappingURL=dispatch.d.ts.map
|