@xfey/tutti 0.1.126 → 0.1.128
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/README.md +2 -0
- package/dist/approvals/decision-command.d.ts +2 -2
- package/dist/chat-assistant/diagnostics.d.ts +11 -3
- package/dist/chat-assistant/diagnostics.js +5 -2
- package/dist/chat-assistant/failures.d.ts +27 -0
- package/dist/chat-assistant/failures.js +102 -0
- package/dist/chat-assistant/index.d.ts +14 -2
- package/dist/chat-assistant/index.js +241 -54
- package/dist/chat-assistant/language-context.d.ts +8 -0
- package/dist/chat-assistant/language-context.js +41 -0
- package/dist/chat-assistant/requests.d.ts +30 -0
- package/dist/chat-assistant/requests.js +58 -0
- package/dist/collaboration-ingestion/clarification.js +3 -1
- package/dist/collaboration-ingestion/main-chat.js +1 -0
- package/dist/collaboration-ingestion/types.d.ts +2 -0
- package/dist/collaboration-state/clarification-snapshot.d.ts +5 -5
- package/dist/collaboration-state/communication-language.d.ts +31 -0
- package/dist/collaboration-state/communication-language.js +36 -0
- package/dist/collaboration-state/task-compile-context.d.ts +1 -1
- package/dist/collaboration-state/task-compile-context.js +20 -1
- package/dist/collaboration-state/task-compile-requests.d.ts +6 -0
- package/dist/collaboration-state/task-compile-requests.js +16 -0
- package/dist/communication-language/index.d.ts +75 -0
- package/dist/communication-language/index.js +122 -0
- package/dist/communication-language/live-smoke-cli.d.ts +2 -0
- package/dist/communication-language/live-smoke-cli.js +368 -0
- package/dist/communication-language/prompt.d.ts +22 -0
- package/dist/communication-language/prompt.js +73 -0
- package/dist/control-plane/communication-language.d.ts +33 -0
- package/dist/control-plane/communication-language.js +72 -0
- package/dist/control-plane/context-sync.js +7 -0
- package/dist/control-plane/follow-up-output.js +25 -4
- package/dist/control-plane/follow-up-start.js +17 -0
- package/dist/control-plane/project-brief-refresh.js +10 -1
- package/dist/control-plane/project-context-bootstrap.d.ts +2 -0
- package/dist/control-plane/project-context-bootstrap.js +8 -0
- package/dist/control-plane/reference-summary-refresh.js +7 -0
- package/dist/control-plane/run-start.js +14 -6
- package/dist/control-plane/scratchpad-refresh-start.d.ts +1 -0
- package/dist/control-plane/scratchpad-refresh-start.js +13 -0
- package/dist/control-plane/scratchpad-refresh.d.ts +2 -0
- package/dist/control-plane/scratchpad-refresh.js +40 -26
- package/dist/control-plane/task-compile-continuation.js +16 -1
- package/dist/control-plane/task-compile-output.js +38 -14
- package/dist/control-plane/task-compile-request-status.d.ts +6 -0
- package/dist/control-plane/task-compile-request-status.js +70 -0
- package/dist/control-plane/task-compile-start.d.ts +1 -0
- package/dist/control-plane/task-compile-start.js +62 -3
- package/dist/control-plane/types.d.ts +2 -0
- package/dist/control-plane/workflows/openai.js +48 -28
- package/dist/control-plane/workflows/types.d.ts +18 -13
- package/dist/prompt-templates/index.d.ts +1 -1
- package/dist/prompt-templates/index.js +2 -0
- package/dist/providers/failure-classification.d.ts +23 -0
- package/dist/providers/failure-classification.js +43 -0
- package/dist/reference-access/read-evidence.d.ts +2 -0
- package/dist/reference-access/read-evidence.js +5 -0
- package/dist/reference-access/service.d.ts +2 -0
- package/dist/reference-access/service.js +2 -0
- package/dist/run-pipeline/task-run-invocation.js +12 -9
- package/dist/runtime-failures.js +15 -1
- package/dist/server-shell/cli/external-reference-runtime.js +6 -3
- package/dist/server-shell/cli/host-server-runtime.js +4 -1
- package/dist/server-shell/cli/relay-registration.js +1 -0
- package/dist/server-shell/http/routes/project-api/messages-routes.js +22 -1
- package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.d.ts +140 -5
- package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.js +29 -2
- package/dist/server-shell/http/routes/project-api/openapi-skills-routes.d.ts +2 -0
- package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +6 -0
- package/dist/server-shell/http/routes/project-api/openapi.d.ts +148 -5
- package/dist/server-shell/http/routes/project-api/schemas.d.ts +6 -0
- package/dist/server-shell/http/routes/project-api/schemas.js +1 -0
- package/dist/server-shell/http/routes/project-api/types.d.ts +1 -0
- package/dist/server-shell/http/routes/project-api/workflow-routes.js +39 -3
- package/dist/server-shell/http/routes/project-api/workspace-projections.js +1 -0
- package/dist/server-shell/http/validation.js +8 -2
- package/dist/server-shell/local-console/managed-project-service.d.ts +1 -1
- package/dist/server-shell/managed-access/transport.d.ts +1 -1
- package/migrations/0023_communication_language.sql +14 -0
- package/migrations/0024_chat_requests.sql +19 -0
- package/migrations/0025_task_compile_requests.sql +50 -0
- package/migrations/README.md +3 -1
- package/node_modules/@tutti/relay-client/dist/host-control.d.ts +1 -0
- package/node_modules/@tutti/relay-client/dist/host-control.js +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/operation-status.d.ts +174 -0
- package/node_modules/@tutti/shared/dist/schemas/api/operation-status.js +75 -0
- package/node_modules/@tutti/shared/dist/schemas/api/primitives.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/primitives.js +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.js +2 -1
- package/package.json +1 -1
- package/prompts/README.md +1 -0
- package/prompts/chat-assistant.md +1 -1
- package/prompts/communication-language-result.md +27 -0
- package/prompts/communication-language.md +50 -0
- package/prompts/procedures/follow-up-check.md +1 -1
- package/prompts/procedures/scratchpad-refresh.md +2 -2
- package/prompts/procedures/task-compile.md +1 -1
- package/prompts/prompt-flow-map.md +2 -0
- package/web/assets/{homepage-motion-scene-DyEmWwvt.js → homepage-motion-scene-BcMt0AVW.js} +1 -1
- package/web/assets/index-2eX37Jx6.js +70 -0
- package/web/assets/index-BJvKuUiT.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-By3idT4U.js +0 -70
- package/web/assets/index-DSBjOZmJ.css +0 -1
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { linkChatRequestInvocation } from "./requests.js";
|
|
2
|
+
import { createEventRef, ID_PREFIXES, isPrefixedId, } from "@tutti/shared/ids";
|
|
1
3
|
import { createChatAssistantDiagnostics } from "./diagnostics.js";
|
|
2
4
|
import { createClarificationRoundAgentMessage, createMainChatAgentMessage, readClarificationRoundMessagesWindow, readClarificationRoundProjection, readMainChatMessageWindow, readMainChatMessagesBeforeCursor, readMessageProjectionById, } from "../collaboration-state/index.js";
|
|
3
|
-
import {
|
|
5
|
+
import { renderLanguageAwarePrompt } from "../communication-language/prompt.js";
|
|
6
|
+
import { languageDiagnosticFields } from "../communication-language/index.js";
|
|
7
|
+
import { chatLanguageContext } from "./language-context.js";
|
|
4
8
|
import { withHostStoreTransaction, } from "../store/index.js";
|
|
5
9
|
import { messageTextForModel, messageTextForTuttiMention, } from "../collaboration-state/message-context.js";
|
|
6
10
|
import { externalReferenceMessageContext } from "../reference-access/message-context.js";
|
|
7
11
|
import { readExternalReferenceReadSources } from "../reference-access/read-evidence.js";
|
|
12
|
+
import { chatFailure, chatFailureMessage, classifyChatFailure } from "./failures.js";
|
|
13
|
+
import { countQueuedChatRequests, insertChatRequest, listRunningChatRequests, nextQueuedChatRequest, readChatRequest, updateChatRequest, } from "./requests.js";
|
|
8
14
|
const CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT = 3;
|
|
9
15
|
const CHAT_ASSISTANT_ROUND_MESSAGE_LIMIT = 50;
|
|
10
16
|
const CHAT_ASSISTANT_ROUND_MESSAGE_MAX_CHARS = 2_000;
|
|
@@ -43,9 +49,6 @@ function normalizeAnswer(answer) {
|
|
|
43
49
|
}
|
|
44
50
|
return `${trimmed.slice(0, CHAT_ASSISTANT_MAX_ANSWER_CHARS - 3)}...`;
|
|
45
51
|
}
|
|
46
|
-
function assistantFailureMessage() {
|
|
47
|
-
return "Tutti could not complete this read-only answer. Try again later, or add more context in chat.";
|
|
48
|
-
}
|
|
49
52
|
function messageToContext(message) {
|
|
50
53
|
const authorDisplayName = message.author.display_name?.trim();
|
|
51
54
|
return {
|
|
@@ -103,46 +106,112 @@ function roundMessagesToContext(messages) {
|
|
|
103
106
|
}
|
|
104
107
|
export class ReadOnlyChatAssistant {
|
|
105
108
|
options;
|
|
106
|
-
pending = [];
|
|
107
109
|
idleWaiters = new Set();
|
|
108
110
|
active = null;
|
|
109
111
|
accepting = true;
|
|
110
112
|
maxQueuedMentions;
|
|
113
|
+
recoveryTimer;
|
|
114
|
+
started = false;
|
|
115
|
+
activeAttemptRef;
|
|
116
|
+
storageFailureReported = false;
|
|
111
117
|
constructor(options) {
|
|
112
118
|
this.options = options;
|
|
113
|
-
this.maxQueuedMentions = options.maxQueuedMentions ?? 8;
|
|
119
|
+
this.maxQueuedMentions = Math.max(1, options.maxQueuedMentions ?? 8);
|
|
114
120
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
/** Call once after Host recovery/transport wiring. No old messages are backfilled. */
|
|
122
|
+
start() {
|
|
123
|
+
if (this.started || !this.accepting)
|
|
124
|
+
return;
|
|
125
|
+
this.started = true;
|
|
126
|
+
for (const row of listRunningChatRequests(this.options.store.db)) {
|
|
127
|
+
if (row.attempt_ref === this.activeAttemptRef)
|
|
128
|
+
continue;
|
|
129
|
+
if (row.phase === "workspace_wait") {
|
|
130
|
+
updateChatRequest(this.options.store.db, row, { status: "queued", phase: "queue" }, this.now());
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const failure = chatFailure("host_restarted");
|
|
134
|
+
const recovered = updateChatRequest(this.options.store.db, row, {
|
|
135
|
+
status: "failed",
|
|
136
|
+
phase: "settled",
|
|
137
|
+
failure,
|
|
138
|
+
}, this.now());
|
|
139
|
+
const message = readMessageProjectionById(this.options.store.db, row.trigger_message_id);
|
|
140
|
+
if (recovered && message) {
|
|
141
|
+
this.report(message, recovered)({ stage: "failed", error_code: "host_restarted", diagnostic_id: failure.diagnostic_id });
|
|
142
|
+
try {
|
|
143
|
+
this.appendAssistantMessageForTrigger(message, chatFailureMessage(failure), recovered, failure);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
this.report(message, recovered)({ stage: "failed", reason_code: "reply_failed" });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
125
149
|
}
|
|
126
|
-
|
|
150
|
+
// A lost post-commit wake must not strand a durable queued request.
|
|
151
|
+
this.recoveryTimer = setInterval(() => this.pump(), 1_000);
|
|
152
|
+
this.recoveryTimer.unref?.();
|
|
153
|
+
this.pump();
|
|
154
|
+
}
|
|
155
|
+
register(tx, message) {
|
|
156
|
+
if (!this.accepting ||
|
|
157
|
+
(message.author.kind !== "human" &&
|
|
158
|
+
!(message.author.kind === "system" && message.refs?.development_operator === true)) ||
|
|
159
|
+
!containsTuttiMention(messageTextForTuttiMention(message)) ||
|
|
160
|
+
readChatRequest(tx, message.id) !== null)
|
|
161
|
+
return;
|
|
162
|
+
insertChatRequest(tx, message.id, this.now(), countQueuedChatRequests(tx) >= this.maxQueuedMentions
|
|
163
|
+
? chatFailure("chat_queue_full")
|
|
164
|
+
: undefined);
|
|
165
|
+
}
|
|
166
|
+
now() {
|
|
167
|
+
return (this.options.now?.() ?? new Date()).toISOString();
|
|
168
|
+
}
|
|
169
|
+
report(message, request) {
|
|
170
|
+
return createChatAssistantDiagnostics({
|
|
127
171
|
...(this.options.logger === undefined ? {} : { logger: this.options.logger }),
|
|
128
172
|
projectId: this.options.project.project_id,
|
|
129
|
-
triggerMessageId:
|
|
130
|
-
scope:
|
|
173
|
+
triggerMessageId: message.id,
|
|
174
|
+
scope: message.scope.kind,
|
|
175
|
+
requestRef: message.id,
|
|
176
|
+
...(request.attempt_ref === null ? {} : { attemptRef: request.attempt_ref }),
|
|
131
177
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
178
|
+
}
|
|
179
|
+
enqueue(input) {
|
|
180
|
+
if (!this.accepting)
|
|
181
|
+
return { triggered: false };
|
|
182
|
+
withHostStoreTransaction(this.options.store.db, (tx) => this.register(tx, input.message));
|
|
183
|
+
const request = readChatRequest(this.options.store.db, input.message.id);
|
|
184
|
+
if (request === null)
|
|
185
|
+
return { triggered: false };
|
|
186
|
+
if (request.status === "failed" &&
|
|
187
|
+
request.response_message_id === null &&
|
|
188
|
+
request.failure_json !== null) {
|
|
189
|
+
const report = this.report(input.message, request);
|
|
190
|
+
const failure = JSON.parse(request.failure_json);
|
|
191
|
+
report({
|
|
192
|
+
stage: "failed",
|
|
193
|
+
reason_code: failure.code === "chat_queue_full" ? "queue_full" : "reply_failed",
|
|
194
|
+
diagnostic_id: failure.diagnostic_id,
|
|
136
195
|
});
|
|
196
|
+
try {
|
|
197
|
+
this.appendAssistantMessageForTrigger(input.message, chatFailureMessage(failure), request, failure);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// The original message and failed request are already accepted.
|
|
201
|
+
report({
|
|
202
|
+
stage: "notification_failed",
|
|
203
|
+
reason_code: "reply_failed",
|
|
204
|
+
diagnostic_id: failure.diagnostic_id,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
137
207
|
return { triggered: true };
|
|
138
208
|
}
|
|
139
|
-
report({ stage: "queued" });
|
|
140
|
-
this.pending.push({ message: input.message, report });
|
|
141
209
|
this.pump();
|
|
142
210
|
return { triggered: true };
|
|
143
211
|
}
|
|
144
212
|
async waitForIdle() {
|
|
145
|
-
if (this.active === null &&
|
|
213
|
+
if (this.active === null &&
|
|
214
|
+
(!this.accepting || nextQueuedChatRequest(this.options.store.db) === null)) {
|
|
146
215
|
return;
|
|
147
216
|
}
|
|
148
217
|
await new Promise((resolve) => this.idleWaiters.add(resolve));
|
|
@@ -152,36 +221,102 @@ export class ReadOnlyChatAssistant {
|
|
|
152
221
|
return;
|
|
153
222
|
}
|
|
154
223
|
this.accepting = false;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.appendAssistantMessageForTrigger(message, assistantFailureMessage(), {
|
|
158
|
-
scratchpad_source: "exclude",
|
|
159
|
-
});
|
|
160
|
-
}
|
|
224
|
+
if (this.recoveryTimer !== undefined)
|
|
225
|
+
clearInterval(this.recoveryTimer);
|
|
161
226
|
this.resolveIdleIfNeeded();
|
|
162
227
|
}
|
|
163
228
|
pump() {
|
|
164
|
-
|
|
229
|
+
try {
|
|
230
|
+
this.pumpNext();
|
|
231
|
+
this.storageFailureReported = false;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
this.reportStorageFailure();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
reportStorageFailure() {
|
|
238
|
+
if (this.storageFailureReported)
|
|
239
|
+
return;
|
|
240
|
+
this.storageFailureReported = true;
|
|
241
|
+
try {
|
|
242
|
+
this.options.logger?.warn({
|
|
243
|
+
project_id: this.options.project.project_id,
|
|
244
|
+
stage: "queue_storage_failed",
|
|
245
|
+
diagnostic_id: createEventRef(),
|
|
246
|
+
}, "Chat request queue storage unavailable");
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
/* Diagnostics cannot replace durable recovery. */
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
pumpNext() {
|
|
253
|
+
if (this.active !== null)
|
|
254
|
+
return;
|
|
255
|
+
if (!this.accepting) {
|
|
256
|
+
this.resolveIdleIfNeeded();
|
|
165
257
|
return;
|
|
166
258
|
}
|
|
167
|
-
const pending = this.
|
|
168
|
-
if (pending ===
|
|
259
|
+
const pending = nextQueuedChatRequest(this.options.store.db);
|
|
260
|
+
if (pending === null) {
|
|
169
261
|
this.resolveIdleIfNeeded();
|
|
170
262
|
return;
|
|
171
263
|
}
|
|
172
|
-
const
|
|
173
|
-
|
|
264
|
+
const request = updateChatRequest(this.options.store.db, pending, {
|
|
265
|
+
status: "running",
|
|
266
|
+
phase: "workspace_wait",
|
|
267
|
+
attemptRef: createEventRef(),
|
|
268
|
+
}, this.now());
|
|
269
|
+
if (request === null)
|
|
270
|
+
return;
|
|
271
|
+
this.activeAttemptRef = request.attempt_ref;
|
|
272
|
+
const message = readMessageProjectionById(this.options.store.db, request.trigger_message_id);
|
|
273
|
+
if (message === null)
|
|
274
|
+
throw new Error("chat_request_source_missing");
|
|
275
|
+
const report = this.report(message, request);
|
|
276
|
+
report({ stage: "queued" });
|
|
277
|
+
const task = this.respond(message, request, report).catch(() => {
|
|
278
|
+
// Do not overwrite a committed success when only notification delivery failed.
|
|
279
|
+
try {
|
|
280
|
+
const current = readChatRequest(this.options.store.db, message.id);
|
|
281
|
+
if (current?.status === "running" && current.attempt_ref === request.attempt_ref) {
|
|
282
|
+
const failure = chatFailure("chat_reply_persistence_failed", request.attempt_ref);
|
|
283
|
+
updateChatRequest(this.options.store.db, current, {
|
|
284
|
+
status: "failed",
|
|
285
|
+
phase: "settled",
|
|
286
|
+
failure,
|
|
287
|
+
}, this.now());
|
|
288
|
+
report({
|
|
289
|
+
stage: "failed",
|
|
290
|
+
reason_code: "reply_failed",
|
|
291
|
+
error_code: "chat_reply_persistence_failed",
|
|
292
|
+
diagnostic_id: failure.diagnostic_id,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
else if (current !== null &&
|
|
296
|
+
current.response_message_id !== null &&
|
|
297
|
+
current.attempt_ref === request.attempt_ref) {
|
|
298
|
+
report({ stage: "notification_failed", reason_code: "reply_failed" });
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
report({ stage: "skipped", reason_code: "request_changed" });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
this.reportStorageFailure();
|
|
306
|
+
}
|
|
174
307
|
});
|
|
175
308
|
this.active = task;
|
|
176
309
|
void task.finally(() => {
|
|
177
310
|
if (this.active === task) {
|
|
178
311
|
this.active = null;
|
|
312
|
+
this.activeAttemptRef = undefined;
|
|
179
313
|
this.pump();
|
|
180
314
|
}
|
|
181
315
|
});
|
|
182
316
|
}
|
|
183
317
|
resolveIdleIfNeeded() {
|
|
184
|
-
if (this.active !== null ||
|
|
318
|
+
if (this.active !== null ||
|
|
319
|
+
(this.accepting && nextQueuedChatRequest(this.options.store.db) !== null)) {
|
|
185
320
|
return;
|
|
186
321
|
}
|
|
187
322
|
for (const resolve of this.idleWaiters) {
|
|
@@ -241,20 +376,27 @@ export class ReadOnlyChatAssistant {
|
|
|
241
376
|
}
|
|
242
377
|
return this.buildMainChatContext(triggerMessage);
|
|
243
378
|
}
|
|
244
|
-
renderPrompt(triggerMessage) {
|
|
379
|
+
renderPrompt(triggerMessage, report) {
|
|
245
380
|
const context = this.buildContext(triggerMessage);
|
|
246
381
|
if (context === null) {
|
|
247
382
|
return null;
|
|
248
383
|
}
|
|
249
|
-
|
|
384
|
+
const language = chatLanguageContext(this.options.store.db, triggerMessage, {
|
|
385
|
+
mainChat: CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT,
|
|
386
|
+
round: CHAT_ASSISTANT_ROUND_MESSAGE_LIMIT,
|
|
387
|
+
});
|
|
388
|
+
report({ stage: "language_context", ...languageDiagnosticFields(language) });
|
|
389
|
+
return renderLanguageAwarePrompt({
|
|
250
390
|
templateKey: "chat.assistant",
|
|
251
|
-
...(this.options.promptsRoot === undefined
|
|
391
|
+
...(this.options.promptsRoot === undefined
|
|
392
|
+
? {}
|
|
393
|
+
: { promptsRoot: this.options.promptsRoot }),
|
|
252
394
|
variables: {
|
|
253
395
|
assistant_context_json: context,
|
|
254
396
|
},
|
|
255
|
-
});
|
|
397
|
+
}, language);
|
|
256
398
|
}
|
|
257
|
-
appendMainChatAssistantMessage(triggerMessage, body, refs) {
|
|
399
|
+
appendMainChatAssistantMessage(triggerMessage, body, request, failure, refs) {
|
|
258
400
|
let afterCommit;
|
|
259
401
|
const message = withHostStoreTransaction(this.options.store.db, (tx) => {
|
|
260
402
|
const responseMessage = createMainChatAgentMessage(tx, {
|
|
@@ -270,6 +412,7 @@ export class ReadOnlyChatAssistant {
|
|
|
270
412
|
if (typeof callback === "function") {
|
|
271
413
|
afterCommit = callback;
|
|
272
414
|
}
|
|
415
|
+
this.recordReply(tx, request, responseMessage.id, failure);
|
|
273
416
|
return responseMessage;
|
|
274
417
|
});
|
|
275
418
|
afterCommit?.();
|
|
@@ -301,6 +444,7 @@ export class ReadOnlyChatAssistant {
|
|
|
301
444
|
});
|
|
302
445
|
if (typeof callback === "function")
|
|
303
446
|
afterCommit = callback;
|
|
447
|
+
this.recordReply(tx, input.request, responseMessage.id, input.failure);
|
|
304
448
|
return responseMessage;
|
|
305
449
|
});
|
|
306
450
|
afterCommit?.();
|
|
@@ -323,36 +467,75 @@ export class ReadOnlyChatAssistant {
|
|
|
323
467
|
invalidates,
|
|
324
468
|
});
|
|
325
469
|
}
|
|
326
|
-
appendAssistantMessageForTrigger(triggerMessage, body, refs) {
|
|
470
|
+
appendAssistantMessageForTrigger(triggerMessage, body, request, failure, refs) {
|
|
471
|
+
if (failure !== undefined)
|
|
472
|
+
refs = { ...refs, scratchpad_source: "exclude" };
|
|
327
473
|
if (triggerMessage.scope.kind === "clarification_round") {
|
|
328
474
|
this.appendClarificationRoundAssistantMessage({
|
|
329
475
|
triggerMessage,
|
|
330
476
|
roundId: triggerMessage.scope.clarification_round_ref,
|
|
331
477
|
body,
|
|
478
|
+
request,
|
|
479
|
+
...(failure === undefined ? {} : { failure }),
|
|
332
480
|
...(refs === undefined ? {} : { refs }),
|
|
333
481
|
});
|
|
334
482
|
return;
|
|
335
483
|
}
|
|
336
|
-
this.appendMainChatAssistantMessage(triggerMessage, body, refs);
|
|
484
|
+
this.appendMainChatAssistantMessage(triggerMessage, body, request, failure, refs);
|
|
485
|
+
}
|
|
486
|
+
recordReply(tx, request, messageId, failure) {
|
|
487
|
+
if (!updateChatRequest(tx, request, {
|
|
488
|
+
status: failure ? "failed" : "succeeded",
|
|
489
|
+
phase: "settled",
|
|
490
|
+
responseMessageId: messageId,
|
|
491
|
+
...(failure === undefined ? {} : { failure }),
|
|
492
|
+
}, this.now()))
|
|
493
|
+
throw new Error("chat_request_changed");
|
|
337
494
|
}
|
|
338
|
-
async respond(triggerMessage, report) {
|
|
495
|
+
async respond(triggerMessage, request, report) {
|
|
339
496
|
let answer;
|
|
340
497
|
let invocationRef;
|
|
341
498
|
let readSources = [];
|
|
342
499
|
try {
|
|
343
500
|
report({ stage: "inspection_wait" });
|
|
344
501
|
await this.options.waitForInspectionAvailability?.();
|
|
345
|
-
|
|
502
|
+
if (!this.accepting) {
|
|
503
|
+
updateChatRequest(this.options.store.db, request, { status: "queued", phase: "queue" }, this.now());
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const prompt = this.renderPrompt(triggerMessage, report);
|
|
346
507
|
if (prompt === null) {
|
|
347
508
|
report({ stage: "skipped", reason_code: "context_unavailable" });
|
|
509
|
+
updateChatRequest(this.options.store.db, request, {
|
|
510
|
+
status: "failed",
|
|
511
|
+
phase: "settled",
|
|
512
|
+
failure: chatFailure("chat_context_unavailable", request.attempt_ref),
|
|
513
|
+
}, this.now());
|
|
348
514
|
return;
|
|
349
515
|
}
|
|
516
|
+
const executing = updateChatRequest(this.options.store.db, request, {
|
|
517
|
+
status: "running",
|
|
518
|
+
phase: "answer",
|
|
519
|
+
}, this.now());
|
|
520
|
+
if (executing === null)
|
|
521
|
+
return;
|
|
522
|
+
request = executing;
|
|
350
523
|
report({ stage: "model_requested" });
|
|
351
524
|
const output = await this.options.model.answer({
|
|
352
525
|
prompt,
|
|
353
526
|
onProgress: (event) => {
|
|
354
|
-
if (event.stage === "context_ready")
|
|
355
|
-
|
|
527
|
+
if (event.stage === "context_ready") {
|
|
528
|
+
if (typeof event.invocation_ref === "string" &&
|
|
529
|
+
isPrefixedId(event.invocation_ref, ID_PREFIXES.event)) {
|
|
530
|
+
invocationRef = event.invocation_ref;
|
|
531
|
+
try {
|
|
532
|
+
linkChatRequestInvocation(this.options.store.db, request, invocationRef);
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
report({ stage: "notification_failed" });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
356
539
|
report(event);
|
|
357
540
|
},
|
|
358
541
|
});
|
|
@@ -361,16 +544,20 @@ export class ReadOnlyChatAssistant {
|
|
|
361
544
|
readSources = readExternalReferenceReadSources(this.options.store.db, invocationRef);
|
|
362
545
|
}
|
|
363
546
|
}
|
|
364
|
-
catch {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
547
|
+
catch (error) {
|
|
548
|
+
const failure = classifyChatFailure(error, request.attempt_ref);
|
|
549
|
+
report({
|
|
550
|
+
stage: "failed",
|
|
551
|
+
reason_code: "answer_failed",
|
|
552
|
+
error_code: failure.code,
|
|
553
|
+
diagnostic_id: failure.diagnostic_id,
|
|
368
554
|
});
|
|
555
|
+
this.appendAssistantMessageForTrigger(triggerMessage, chatFailureMessage(failure), request, failure);
|
|
369
556
|
report({ stage: "reply_persisted" });
|
|
370
557
|
return;
|
|
371
558
|
}
|
|
372
559
|
// Persistence/outbound-hook failure must not retry by creating a second reply.
|
|
373
|
-
this.appendAssistantMessageForTrigger(triggerMessage, answer, readSources.length === 0 ? undefined : { external_reference_reads: readSources });
|
|
560
|
+
this.appendAssistantMessageForTrigger(triggerMessage, answer, request, undefined, readSources.length === 0 ? undefined : { external_reference_reads: readSources });
|
|
374
561
|
report({ stage: "reply_persisted", reference_source_count: readSources.length });
|
|
375
562
|
}
|
|
376
563
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MessageProjection } from "@tutti/shared/schemas/api";
|
|
2
|
+
import type { SqliteDatabase } from "../store/index.js";
|
|
3
|
+
/** Same compact scope as Q&A, never boundary-after main chat for a round. */
|
|
4
|
+
export declare function chatLanguageContext(db: SqliteDatabase, trigger: MessageProjection, limits: {
|
|
5
|
+
mainChat: number;
|
|
6
|
+
round: number;
|
|
7
|
+
}): import("../communication-language/index.js").CommunicationLanguageContext;
|
|
8
|
+
//# sourceMappingURL=language-context.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readActiveClarificationProjection, readClarificationRoundMessagesWindow, readMainChatMessageWindow, readTaskProjection, } from "../collaboration-state/index.js";
|
|
2
|
+
import { readCommunicationLanguage } from "../collaboration-state/communication-language.js";
|
|
3
|
+
import { createLanguageContext, messageLanguageEvidence } from "../communication-language/index.js";
|
|
4
|
+
/** Same compact scope as Q&A, never boundary-after main chat for a round. */
|
|
5
|
+
export function chatLanguageContext(db, trigger, limits) {
|
|
6
|
+
let inherited = readCommunicationLanguage(db);
|
|
7
|
+
let messages;
|
|
8
|
+
if (trigger.scope.kind === "main_chat") {
|
|
9
|
+
messages = readMainChatMessageWindow(db, { limit: limits.mainChat }).items;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
messages =
|
|
13
|
+
readClarificationRoundMessagesWindow(db, trigger.scope.clarification_round_ref, {
|
|
14
|
+
anchor: "start",
|
|
15
|
+
limit: limits.round,
|
|
16
|
+
})?.items ?? [];
|
|
17
|
+
const active = readActiveClarificationProjection(db);
|
|
18
|
+
// A task/compile round must not inherit a newer project-wide language choice.
|
|
19
|
+
inherited = null;
|
|
20
|
+
if (active.state === "active" && active.round.id === trigger.scope.clarification_round_ref) {
|
|
21
|
+
const owner = active.thread.owner;
|
|
22
|
+
const task = owner.kind === "task" ? readTaskProjection(db, owner.task_id) : null;
|
|
23
|
+
const workflowRef = owner.kind === "task_compile" ? owner.workflow_ref : task?.created_from_workflow_ref;
|
|
24
|
+
inherited =
|
|
25
|
+
(task === null ? null : readCommunicationLanguage(db, { kind: "task", ref: task.id })) ??
|
|
26
|
+
(workflowRef === undefined
|
|
27
|
+
? null
|
|
28
|
+
: readCommunicationLanguage(db, { kind: "workflow", ref: workflowRef }));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return createLanguageContext({
|
|
32
|
+
scope: "reply",
|
|
33
|
+
inherited,
|
|
34
|
+
messages: [
|
|
35
|
+
...messages.filter((message) => message.created_at < trigger.created_at ||
|
|
36
|
+
(message.created_at === trigger.created_at && message.id < trigger.id)),
|
|
37
|
+
trigger,
|
|
38
|
+
].map(messageLanguageEvidence),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=language-context.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ChatRequestStatus, type OperationFailure } from "@tutti/shared/schemas/api";
|
|
2
|
+
import type { SqliteDatabase } from "../store/index.js";
|
|
3
|
+
export type ChatRequestRecord = {
|
|
4
|
+
trigger_message_id: string;
|
|
5
|
+
status: "queued" | "running" | "succeeded" | "failed";
|
|
6
|
+
phase: "queue" | "workspace_wait" | "answer" | "settled";
|
|
7
|
+
revision: number;
|
|
8
|
+
attempt_ref: string | null;
|
|
9
|
+
response_message_id: string | null;
|
|
10
|
+
failure_json: string | null;
|
|
11
|
+
created_at: string;
|
|
12
|
+
updated_at: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function readChatRequest(db: SqliteDatabase, messageId: string): ChatRequestRecord | null;
|
|
15
|
+
export declare function chatRequestStatus(row: ChatRequestRecord): ChatRequestStatus;
|
|
16
|
+
export declare function insertChatRequest(db: SqliteDatabase, messageId: string, now: string, failure?: OperationFailure): void;
|
|
17
|
+
export declare function countQueuedChatRequests(db: SqliteDatabase): number;
|
|
18
|
+
export declare function nextQueuedChatRequest(db: SqliteDatabase): ChatRequestRecord | null;
|
|
19
|
+
/** CAS/fencing only. Lifecycle policy belongs to Chat Assistant, not the repository. */
|
|
20
|
+
export declare function updateChatRequest(db: SqliteDatabase, row: ChatRequestRecord, next: {
|
|
21
|
+
status: ChatRequestRecord["status"];
|
|
22
|
+
phase: ChatRequestRecord["phase"];
|
|
23
|
+
attemptRef?: string;
|
|
24
|
+
responseMessageId?: string;
|
|
25
|
+
failure?: OperationFailure;
|
|
26
|
+
}, now: string): ChatRequestRecord | null;
|
|
27
|
+
export declare function listRunningChatRequests(db: SqliteDatabase): ChatRequestRecord[];
|
|
28
|
+
/** Correlation only; never changes lifecycle state based on a progress callback. */
|
|
29
|
+
export declare function linkChatRequestInvocation(db: SqliteDatabase, row: ChatRequestRecord, invocationRef: string): void;
|
|
30
|
+
//# sourceMappingURL=requests.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Value } from "@sinclair/typebox/value";
|
|
2
|
+
import { ChatRequestStatusSchema, } from "@tutti/shared/schemas/api";
|
|
3
|
+
export function readChatRequest(db, messageId) {
|
|
4
|
+
return (db
|
|
5
|
+
.prepare("SELECT * FROM chat_requests WHERE trigger_message_id = ?")
|
|
6
|
+
.get(messageId) ?? null);
|
|
7
|
+
}
|
|
8
|
+
export function chatRequestStatus(row) {
|
|
9
|
+
const value = {
|
|
10
|
+
operation: { kind: "chat_answer", ref: row.trigger_message_id },
|
|
11
|
+
status: row.status,
|
|
12
|
+
phase: row.phase,
|
|
13
|
+
revision: row.revision,
|
|
14
|
+
created_at: row.created_at,
|
|
15
|
+
updated_at: row.updated_at,
|
|
16
|
+
...(row.response_message_id === null ? {} : { response_message_id: row.response_message_id }),
|
|
17
|
+
...(row.failure_json === null ? {} : { failure: JSON.parse(row.failure_json) }),
|
|
18
|
+
};
|
|
19
|
+
if (!Value.Check(ChatRequestStatusSchema, value))
|
|
20
|
+
throw new Error("chat_request_invalid");
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
export function insertChatRequest(db, messageId, now, failure) {
|
|
24
|
+
db.prepare(`INSERT INTO chat_requests
|
|
25
|
+
(trigger_message_id, status, phase, failure_json, created_at, updated_at)
|
|
26
|
+
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(trigger_message_id) DO NOTHING`).run(messageId, failure ? "failed" : "queued", failure ? "settled" : "queue", failure ? JSON.stringify(failure) : null, now, now);
|
|
27
|
+
}
|
|
28
|
+
export function countQueuedChatRequests(db) {
|
|
29
|
+
return db
|
|
30
|
+
.prepare("SELECT count(*) AS count FROM chat_requests WHERE status = 'queued'")
|
|
31
|
+
.get().count;
|
|
32
|
+
}
|
|
33
|
+
export function nextQueuedChatRequest(db) {
|
|
34
|
+
return (db
|
|
35
|
+
.prepare("SELECT * FROM chat_requests WHERE status = 'queued' ORDER BY enqueue_order LIMIT 1")
|
|
36
|
+
.get() ?? null);
|
|
37
|
+
}
|
|
38
|
+
/** CAS/fencing only. Lifecycle policy belongs to Chat Assistant, not the repository. */
|
|
39
|
+
export function updateChatRequest(db, row, next, now) {
|
|
40
|
+
const changed = db
|
|
41
|
+
.prepare(`UPDATE chat_requests SET status = ?, phase = ?,
|
|
42
|
+
attempt_ref = ?, response_message_id = ?, failure_json = ?, updated_at = ?, revision = revision + 1
|
|
43
|
+
WHERE trigger_message_id = ? AND revision = ?`)
|
|
44
|
+
.run(next.status, next.phase, next.attemptRef ?? row.attempt_ref, next.responseMessageId ?? null, next.failure ? JSON.stringify(next.failure) : null, now, row.trigger_message_id, row.revision).changes;
|
|
45
|
+
return changed === 0 ? null : readChatRequest(db, row.trigger_message_id);
|
|
46
|
+
}
|
|
47
|
+
export function listRunningChatRequests(db) {
|
|
48
|
+
return db
|
|
49
|
+
.prepare("SELECT * FROM chat_requests WHERE status = 'running' ORDER BY created_at, trigger_message_id")
|
|
50
|
+
.all();
|
|
51
|
+
}
|
|
52
|
+
/** Correlation only; never changes lifecycle state based on a progress callback. */
|
|
53
|
+
export function linkChatRequestInvocation(db, row, invocationRef) {
|
|
54
|
+
db.prepare(`INSERT OR IGNORE INTO chat_request_invocations(message_id, attempt_ref, invocation_ref)
|
|
55
|
+
SELECT trigger_message_id, attempt_ref, ? FROM chat_requests
|
|
56
|
+
WHERE trigger_message_id = ? AND attempt_ref = ? AND status = 'running'`).run(invocationRef, row.trigger_message_id, row.attempt_ref);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=requests.js.map
|
|
@@ -21,8 +21,10 @@ export function ingestClarificationMessage(options) {
|
|
|
21
21
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
22
22
|
execute: (tx) => {
|
|
23
23
|
const result = options.controlPlane.sendClarificationRoundMessage(options.roundId, options.payload, options.author);
|
|
24
|
-
if (result.result?.message !== undefined)
|
|
24
|
+
if (result.result?.message !== undefined) {
|
|
25
25
|
options.afterCreate?.(tx, result.result.message);
|
|
26
|
+
options.chatAssistant?.register(tx, result.result.message);
|
|
27
|
+
}
|
|
26
28
|
return result;
|
|
27
29
|
},
|
|
28
30
|
});
|
|
@@ -39,6 +39,7 @@ export function ingestMainChatMessage(options) {
|
|
|
39
39
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
40
40
|
});
|
|
41
41
|
options.afterCreate?.(tx, message);
|
|
42
|
+
options.chatAssistant?.register(tx, message);
|
|
42
43
|
return {
|
|
43
44
|
disposition: { kind: "created", message_id: message.id },
|
|
44
45
|
result: { message },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AccountAvatarProjection } from "@tutti/shared/domain";
|
|
2
2
|
import type { MessageProjection } from "@tutti/shared/schemas/api";
|
|
3
|
+
import type { SqliteDatabase } from "../store/index.js";
|
|
3
4
|
export type TrustedCollaborationAuthor = {
|
|
4
5
|
kind?: "human" | "development_operator";
|
|
5
6
|
account_ref: string;
|
|
@@ -10,6 +11,7 @@ export type ScratchpadSourceNotifier = {
|
|
|
10
11
|
notify: (message: MessageProjection) => void;
|
|
11
12
|
};
|
|
12
13
|
export type ChatAssistantEnqueuer = {
|
|
14
|
+
register: (tx: SqliteDatabase, message: MessageProjection) => void;
|
|
13
15
|
enqueue: (input: {
|
|
14
16
|
message: MessageProjection;
|
|
15
17
|
}) => unknown;
|
|
@@ -13,8 +13,9 @@ export declare function readSubmittedClarificationMessages(db: SqliteDatabase, r
|
|
|
13
13
|
is_current_round: boolean;
|
|
14
14
|
};
|
|
15
15
|
refs?: {
|
|
16
|
-
task_id?: string;
|
|
17
16
|
activity_ref?: string;
|
|
17
|
+
workflow_ref?: string;
|
|
18
|
+
task_id?: string;
|
|
18
19
|
artifact_feedback?: {
|
|
19
20
|
summary?: string;
|
|
20
21
|
kind: "artifact_feedback";
|
|
@@ -106,15 +107,14 @@ export declare function readSubmittedClarificationMessages(db: SqliteDatabase, r
|
|
|
106
107
|
blob_oid?: string;
|
|
107
108
|
path: string;
|
|
108
109
|
name: string;
|
|
109
|
-
source: "human_upload" | "tutti_output";
|
|
110
110
|
category: "file" | "image";
|
|
111
|
+
source: "human_upload" | "tutti_output";
|
|
111
112
|
size_bytes: number;
|
|
112
113
|
text_read: {
|
|
113
114
|
blocked_reason?: "unknown" | "binary" | "too_large" | "unsupported_encoding" | "restricted_path" | "secret_like";
|
|
114
115
|
can_read: boolean;
|
|
115
116
|
};
|
|
116
117
|
};
|
|
117
|
-
workflow_ref?: string;
|
|
118
118
|
clarification_round_ref?: string;
|
|
119
119
|
clarification_thread_ref?: string;
|
|
120
120
|
task_result?: {
|
|
@@ -141,10 +141,10 @@ export declare function readSubmittedClarificationMessages(db: SqliteDatabase, r
|
|
|
141
141
|
};
|
|
142
142
|
worklist_compile?: {
|
|
143
143
|
kind: "worklist_compile";
|
|
144
|
-
title: string;
|
|
145
144
|
workflow_ref: string;
|
|
145
|
+
title: string;
|
|
146
146
|
tasks: {
|
|
147
|
-
status: "failed" | "pending" | "
|
|
147
|
+
status: "failed" | "pending" | "running" | "done" | "todo";
|
|
148
148
|
task_id: string;
|
|
149
149
|
title: string;
|
|
150
150
|
summary: string;
|