@jini-ai/daemon 0.5.4 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-executor/contracts.d.ts +37 -5
- package/dist/agent-executor/event-translation.js +4 -0
- package/dist/agent-executor/events.d.ts +17 -2
- package/dist/agent-executor/events.js +245 -8
- package/dist/agent-executor/index.d.ts +2 -2
- package/dist/agent-executor/index.js +1 -1
- package/dist/agent-executor/launch.d.ts +11 -2
- package/dist/agent-executor/launch.js +62 -15
- package/dist/agent-executor/mcp-bridge.js +19 -6
- package/dist/agent-executor.d.ts +1 -1
- package/dist/agent-executor.js +1 -1
- package/dist/http/index.d.ts +5 -4
- package/dist/http/index.js +3 -2
- package/dist/http/research-tavily.adapter.d.ts +6 -0
- package/dist/http/research-tavily.adapter.js +109 -0
- package/dist/http/research.d.ts +20 -2
- package/dist/http/research.js +64 -149
- package/dist/http/route-manifest.js +2 -2
- package/dist/http/runs.d.ts +27 -2
- package/dist/http/runs.js +36 -1
- package/package.json +13 -13
package/dist/http/research.js
CHANGED
|
@@ -96,17 +96,11 @@ import { redactSecrets } from '@jini-ai/core';
|
|
|
96
96
|
* generic-message substitution.
|
|
97
97
|
*/
|
|
98
98
|
import { randomUUID } from 'node:crypto';
|
|
99
|
-
import {
|
|
99
|
+
import { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
100
100
|
import { createApiError } from '@jini-ai/protocol';
|
|
101
101
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
102
102
|
import { validationError } from '@jini-ai/http-kit';
|
|
103
103
|
import { err, ok } from '@jini-ai/http-kit';
|
|
104
|
-
const DEFAULT_TAVILY_BASE_URL = 'https://api.tavily.com';
|
|
105
|
-
/** Tavily's own documented cap on `max_results` (see OD's `tavily.ts#TAVILY_MAX_RESULTS_LIMIT`). */
|
|
106
|
-
const TAVILY_MAX_RESULTS_LIMIT = 20;
|
|
107
|
-
const DEFAULT_MAX_SOURCES = 5;
|
|
108
|
-
/** Matches OD's own `tavily.ts` timeout — see module doc. */
|
|
109
|
-
const DEFAULT_TAVILY_TIMEOUT_MS = 30_000;
|
|
110
104
|
/** Matches OD's real `research/index.ts#searchResearch` query cap — see module doc. */
|
|
111
105
|
const MAX_QUERY_LENGTH = 1000;
|
|
112
106
|
function defaultInternalErrorSink(context) {
|
|
@@ -123,8 +117,8 @@ function defaultInternalErrorSink(context) {
|
|
|
123
117
|
* other provider name at parse time. A host that supports a second provider injects its own
|
|
124
118
|
* `resolveCredentials` rather than reaching this function with a different id.
|
|
125
119
|
*/
|
|
126
|
-
async function defaultResolveCredentials() {
|
|
127
|
-
const apiKey =
|
|
120
|
+
async function defaultResolveCredentials({ env }) {
|
|
121
|
+
const apiKey = env.TAVILY_API_KEY?.trim();
|
|
128
122
|
return apiKey ? { apiKey } : {};
|
|
129
123
|
}
|
|
130
124
|
function isRecord(value) {
|
|
@@ -138,17 +132,17 @@ function nonEmptyString(value) {
|
|
|
138
132
|
* doc). OD only inspects `providers[0]` and treats a non-array `providers` value as absent rather
|
|
139
133
|
* than rejecting it; this mirrors both.
|
|
140
134
|
*/
|
|
141
|
-
function validateRequestedProvider(rawProviders) {
|
|
135
|
+
function validateRequestedProvider(rawProviders, providerId) {
|
|
142
136
|
if (!Array.isArray(rawProviders))
|
|
143
137
|
return ok({ value: null });
|
|
144
138
|
const filtered = rawProviders.filter((p) => typeof p === 'string' && p.length > 0);
|
|
145
139
|
const requested = filtered[0];
|
|
146
|
-
if (requested !== undefined && requested !==
|
|
147
|
-
return err({ error: validationError({ message: `provider "${requested}" is not supported — only "
|
|
140
|
+
if (requested !== undefined && requested !== providerId) {
|
|
141
|
+
return err({ error: validationError({ message: `provider "${requested}" is not supported — only "${providerId}" is available` }) });
|
|
148
142
|
}
|
|
149
143
|
return ok({ value: null });
|
|
150
144
|
}
|
|
151
|
-
function parseResearchSearch(input) {
|
|
145
|
+
function parseResearchSearch(input, providerId = 'tavily') {
|
|
152
146
|
if (!isRecord(input.body))
|
|
153
147
|
return err({ error: validationError({ message: 'body must be a JSON object' }) });
|
|
154
148
|
const rawQuery = nonEmptyString(input.body.query);
|
|
@@ -159,106 +153,11 @@ function parseResearchSearch(input) {
|
|
|
159
153
|
if (maxSources !== undefined && (typeof maxSources !== 'number' || !Number.isFinite(maxSources) || maxSources <= 0)) {
|
|
160
154
|
return err({ error: validationError({ message: 'maxSources must be a positive number when provided' }) });
|
|
161
155
|
}
|
|
162
|
-
const providerCheck = validateRequestedProvider(input.body.providers);
|
|
156
|
+
const providerCheck = validateRequestedProvider(input.body.providers, providerId);
|
|
163
157
|
if (!providerCheck.ok)
|
|
164
158
|
return providerCheck;
|
|
165
159
|
return ok({ value: { query, ...(maxSources !== undefined ? { maxSources: maxSources } : {}) } });
|
|
166
160
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Calls Tavily's real `POST /search` endpoint directly — see module doc for the port's exact
|
|
169
|
-
* scope/provenance. Throws (never a `Result`) on any failure; the route handler below is the one
|
|
170
|
-
* place that converts a thrown error into the package's SEC-005 response shape.
|
|
171
|
-
*
|
|
172
|
-
* `apiKey` is a separate, non-optional parameter rather than being read off `credentials`, so the
|
|
173
|
-
* "credential is present" invariant is carried by the type system instead of by a runtime guard.
|
|
174
|
-
* This function used to take only `credentials` and re-check `credentials.apiKey` itself, but the
|
|
175
|
-
* sole caller below already returns `NOT_CONFIGURED` before it ever gets here — the check was
|
|
176
|
-
* unreachable by construction, so it is now expressed as a required argument instead of dead code.
|
|
177
|
-
* `credentials` is still passed for its optional transport config (`baseUrl`).
|
|
178
|
-
*/
|
|
179
|
-
async function tavilySearch(apiKey, credentials, request) {
|
|
180
|
-
const baseUrl = credentials.baseUrl ?? DEFAULT_TAVILY_BASE_URL;
|
|
181
|
-
const baseUrlCheck = validateBaseUrl({ baseUrl });
|
|
182
|
-
if (baseUrlCheck.error) {
|
|
183
|
-
throw new Error(baseUrlCheck.error);
|
|
184
|
-
}
|
|
185
|
-
const base = baseUrl.replace(/\/+$/, '');
|
|
186
|
-
// Floor before clamping, matching OD's real `clampMaxSources` — see module doc. Without the
|
|
187
|
-
// floor, a fractional `maxSources` (e.g. `2.7`, or `0.5` before the `Math.max(1, ...)` floor
|
|
188
|
-
// below) would forward a non-integer `max_results` straight to Tavily's real API.
|
|
189
|
-
const maxResults = Math.max(1, Math.min(Math.floor(request.maxSources ?? DEFAULT_MAX_SOURCES), TAVILY_MAX_RESULTS_LIMIT));
|
|
190
|
-
const body = {
|
|
191
|
-
query: request.query,
|
|
192
|
-
search_depth: 'basic',
|
|
193
|
-
max_results: maxResults,
|
|
194
|
-
include_answer: true,
|
|
195
|
-
include_raw_content: false,
|
|
196
|
-
};
|
|
197
|
-
// The timeout must stay armed for the full operation, not just until `fetch` resolves —
|
|
198
|
-
// `fetch` resolves as soon as response headers arrive, before the body is read. A round-2
|
|
199
|
-
// external audit caught (and locally reproduced) an earlier version of this fix that cleared
|
|
200
|
-
// the timer right after `fetch` resolved, leaving `response.text()`/`response.json()` below
|
|
201
|
-
// completely unbounded against a server that sends headers promptly and then stalls the body.
|
|
202
|
-
//
|
|
203
|
-
// `TavilyHttpError` marks an error already redacted and formatted by the `!response.ok` branch
|
|
204
|
-
// below, so the outer catch can rethrow it verbatim instead of wrapping it a second time (which
|
|
205
|
-
// would otherwise produce a "Tavily request failed: Tavily 429: ..." double-prefixed message).
|
|
206
|
-
class TavilyHttpError extends Error {
|
|
207
|
-
}
|
|
208
|
-
const timeoutController = new AbortController();
|
|
209
|
-
const timeoutHandle = setTimeout(() => timeoutController.abort(), DEFAULT_TAVILY_TIMEOUT_MS);
|
|
210
|
-
// Assigned in the `try` and returned *after* the `finally`, rather than returned from inside the
|
|
211
|
-
// `try`. Both forms behave identically, but a return-through-`finally` compiles to three copies of
|
|
212
|
-
// the `finally` body (normal completion, return, throw) and one of those is unreachable here — the
|
|
213
|
-
// `try` cannot fall off its end and the `catch` always throws. Falling through to a single return
|
|
214
|
-
// keeps the cleanup path honestly measurable instead of permanently one-third uncovered.
|
|
215
|
-
let output;
|
|
216
|
-
try {
|
|
217
|
-
const response = await fetch(`${base}/search`, {
|
|
218
|
-
method: 'POST',
|
|
219
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
220
|
-
body: JSON.stringify(body),
|
|
221
|
-
signal: timeoutController.signal,
|
|
222
|
-
});
|
|
223
|
-
if (!response.ok) {
|
|
224
|
-
const text = await response.text().catch(() => '');
|
|
225
|
-
throw new TavilyHttpError(redactSecrets({ input: `Tavily ${response.status}: ${text.slice(0, 200) || 'no body'}` }, { exactSecrets: [apiKey] }));
|
|
226
|
-
}
|
|
227
|
-
const json = (await response.json());
|
|
228
|
-
const answer = typeof json.answer === 'string' ? json.answer : '';
|
|
229
|
-
const rawResults = Array.isArray(json.results) ? json.results : [];
|
|
230
|
-
const sources = [];
|
|
231
|
-
for (const raw of rawResults) {
|
|
232
|
-
const url = typeof raw.url === 'string' ? raw.url : '';
|
|
233
|
-
if (!url)
|
|
234
|
-
continue;
|
|
235
|
-
const publishedAt = typeof raw.published_date === 'string' && raw.published_date.trim() ? raw.published_date.trim() : undefined;
|
|
236
|
-
sources.push({
|
|
237
|
-
title: typeof raw.title === 'string' && raw.title.trim() ? raw.title.trim() : url,
|
|
238
|
-
url,
|
|
239
|
-
snippet: typeof raw.content === 'string' ? raw.content.trim().slice(0, 800) : '',
|
|
240
|
-
provider: 'tavily',
|
|
241
|
-
...(publishedAt ? { publishedAt } : {}),
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
output = { answer, sources };
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
if (!timeoutController.signal.aborted && error instanceof TavilyHttpError) {
|
|
248
|
-
throw error;
|
|
249
|
-
}
|
|
250
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
251
|
-
throw new Error(redactSecrets({
|
|
252
|
-
input: timeoutController.signal.aborted
|
|
253
|
-
? `Tavily request timed out after ${DEFAULT_TAVILY_TIMEOUT_MS}ms`
|
|
254
|
-
: `Tavily request failed: ${message}`,
|
|
255
|
-
}, { exactSecrets: [apiKey] }));
|
|
256
|
-
}
|
|
257
|
-
finally {
|
|
258
|
-
clearTimeout(timeoutHandle);
|
|
259
|
-
}
|
|
260
|
-
return output;
|
|
261
|
-
}
|
|
262
161
|
/** Ported verbatim from OD's real `research/index.ts#synthesizeFallbackSummary` — see module doc. Used when Tavily returns no `answer` text so the caller still gets a usable `summary`. */
|
|
263
162
|
function synthesizeFallbackSummary(sources) {
|
|
264
163
|
const lead = sources
|
|
@@ -267,49 +166,65 @@ function synthesizeFallbackSummary(sources) {
|
|
|
267
166
|
.join('\n');
|
|
268
167
|
return `(No provider summary; top snippets follow.)\n${lead}`;
|
|
269
168
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
169
|
+
/** Builds one route over a host-selected provider, preserving the default Tavily validation contract. */
|
|
170
|
+
export function createResearchSearchRoute({ provider }, _optional = {}) {
|
|
171
|
+
const providerId = provider?.id ?? 'tavily';
|
|
172
|
+
return defineJsonRoute({
|
|
173
|
+
method: 'post', path: '/api/research/search', parse: (input) => parseResearchSearch(input, providerId), handle: async ({ input: request, deps }) => {
|
|
174
|
+
const selectedProvider = provider ?? deps.provider ?? createTavilyResearchProvider({}, deps.fetch ? { fetch: deps.fetch } : {});
|
|
175
|
+
const resolveCredentials = deps.resolveCredentials ?? (() => selectedProvider.id === 'tavily' ? defaultResolveCredentials({ env: deps.env ?? process.env }) : Promise.resolve({}));
|
|
176
|
+
const onInternalError = deps.onInternalError ?? defaultInternalErrorSink;
|
|
177
|
+
let credentials;
|
|
178
|
+
try {
|
|
179
|
+
credentials = await resolveCredentials({ providerId: selectedProvider.id });
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const correlationId = randomUUID();
|
|
183
|
+
onInternalError({ correlationId, error });
|
|
184
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
185
|
+
}
|
|
186
|
+
const apiKey = credentials.apiKey;
|
|
187
|
+
if (!apiKey) {
|
|
188
|
+
return err({ error: createApiError({ code: 'NOT_CONFIGURED', message: `${selectedProvider.id} provider not configured` }) });
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const raw = await selectedProvider.search({ apiKey, credentials, request }, {});
|
|
192
|
+
// Matches OD's real `NO_RESEARCH_SOURCES` (404) — see module doc. Zero usable sources is a
|
|
193
|
+
// failure, not an empty success, even when the upstream Tavily call itself succeeded.
|
|
194
|
+
if (raw.sources.length === 0) {
|
|
195
|
+
return err({ error: createApiError({ code: 'NOT_FOUND', message: 'no sources found' }) });
|
|
196
|
+
}
|
|
197
|
+
const result = {
|
|
198
|
+
query: request.query,
|
|
199
|
+
summary: raw.answer || synthesizeFallbackSummary(raw.sources),
|
|
200
|
+
sources: raw.sources,
|
|
201
|
+
provider: selectedProvider.id,
|
|
202
|
+
depth: 'shallow',
|
|
203
|
+
fetchedAt: Date.now(),
|
|
204
|
+
};
|
|
205
|
+
return ok({ value: result });
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const correlationId = randomUUID();
|
|
209
|
+
// A host provider can throw transport text too; keep its configured key out of the sink.
|
|
210
|
+
const safeMessage = redactSecrets({ input: error instanceof Error ? error.message : String(error) }, { exactSecrets: [apiKey] });
|
|
211
|
+
const safeError = new Error(safeMessage);
|
|
212
|
+
if (error instanceof Error) {
|
|
213
|
+
safeError.name = redactSecrets({ input: error.name }, { exactSecrets: [apiKey] });
|
|
214
|
+
if (error.stack === undefined)
|
|
215
|
+
delete safeError.stack;
|
|
216
|
+
else
|
|
217
|
+
safeError.stack = redactSecrets({ input: error.stack }, { exactSecrets: [apiKey] });
|
|
218
|
+
}
|
|
219
|
+
onInternalError({ correlationId, error: safeError });
|
|
220
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
293
221
|
}
|
|
294
|
-
const result = {
|
|
295
|
-
query: request.query,
|
|
296
|
-
summary: raw.answer || synthesizeFallbackSummary(raw.sources),
|
|
297
|
-
sources: raw.sources,
|
|
298
|
-
provider: 'tavily',
|
|
299
|
-
depth: 'shallow',
|
|
300
|
-
fetchedAt: Date.now(),
|
|
301
|
-
};
|
|
302
|
-
return ok({ value: result });
|
|
303
|
-
}
|
|
304
|
-
catch (error) {
|
|
305
|
-
const correlationId = randomUUID();
|
|
306
|
-
onInternalError({ correlationId, error });
|
|
307
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
308
222
|
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
223
|
+
}, { requireSameOrigin: true });
|
|
224
|
+
}
|
|
225
|
+
export const researchSearchRoute = createResearchSearchRoute({});
|
|
311
226
|
/** Mounts `POST /api/research/search` on `app`. A pack's `http(app, services)` calls this directly. */
|
|
312
227
|
export function registerResearchRoutes({ app, deps, adapter }, _optional = {}) {
|
|
313
|
-
mountJsonRoute({ app, spec: researchSearchRoute, deps, adapter });
|
|
228
|
+
mountJsonRoute({ app, spec: deps.provider ? createResearchSearchRoute({ provider: deps.provider }) : researchSearchRoute, deps, adapter });
|
|
314
229
|
}
|
|
315
230
|
//# sourceMappingURL=research.js.map
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
import { agentListRoute, agentRescanRoute } from './agents.js';
|
|
32
32
|
import { delegatedToolExecuteRoute } from './delegated-tools.js';
|
|
33
33
|
import { apiHealthRoute, apiReadyRoute, apiVersionInfoRoute, healthRoute, readyRoute, versionInfoRoute, } from './health.js';
|
|
34
|
-
import { RUN_EVENTS_ROUTE_PATH, runCancelRoute, runListRoute, runStartRoute, runStatusRoute, } from './runs.js';
|
|
34
|
+
import { RUN_EVENTS_ROUTE_PATH, runCancelRoute, runMessageRoute, runListRoute, runStartRoute, runStatusRoute, } from './runs.js';
|
|
35
35
|
import { toolCatalogDescribeRoute, toolCatalogSearchRoute } from './tool-catalog.js';
|
|
36
36
|
/**
|
|
37
37
|
* Projects route specs onto `RouteRegistration`s, upper-casing the method so entries compare directly
|
|
@@ -52,7 +52,7 @@ function fromSpecs(...specs) {
|
|
|
52
52
|
export const JINI_ROUTE_MANIFEST = {
|
|
53
53
|
health: fromSpecs(healthRoute, apiHealthRoute, readyRoute, apiReadyRoute, versionInfoRoute, apiVersionInfoRoute),
|
|
54
54
|
runs: [
|
|
55
|
-
...fromSpecs(runStartRoute, runListRoute, runStatusRoute, runCancelRoute),
|
|
55
|
+
...fromSpecs(runStartRoute, runListRoute, runStatusRoute, runCancelRoute, runMessageRoute),
|
|
56
56
|
// Registered with a bare `app.get` rather than a route spec (by `registerRunRoutes` itself), so
|
|
57
57
|
// it contributes its exported path constant instead of a copied literal.
|
|
58
58
|
{ method: 'GET', path: RUN_EVENTS_ROUTE_PATH },
|
package/dist/http/runs.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Express } from 'express';
|
|
2
2
|
import { type RunStatus } from '@jini-ai/protocol';
|
|
3
|
-
import type { RunLifecycle } from '../index.js';
|
|
3
|
+
import type { RunLifecycle, UserMessageDelivery } from '../index.js';
|
|
4
4
|
import { type AdapterContext } from '@jini-ai/http-kit';
|
|
5
5
|
import type { ServerResponse } from 'node:http';
|
|
6
6
|
/**
|
|
@@ -34,6 +34,15 @@ export interface RunHttpDeps {
|
|
|
34
34
|
readonly onStarted?: RunStartHandler;
|
|
35
35
|
/** Host-owned sink for the real exception behind a generic `INTERNAL_ERROR` response (SEC-005). Defaults to `console.error`. */
|
|
36
36
|
readonly onInternalError?: (context: RunInternalErrorContext) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Delivers a message the human sent mid-run into that run's live agent — a host passes its
|
|
39
|
+
* `AgentExecutor.sendUserMessage`. Omitted, every such message reports `'unsupported'`, so the
|
|
40
|
+
* client falls back to interrupting the run and sending the text as the next turn.
|
|
41
|
+
*/
|
|
42
|
+
readonly deliverUserMessage?: (input: {
|
|
43
|
+
readonly runId: string;
|
|
44
|
+
readonly text: string;
|
|
45
|
+
}) => UserMessageDelivery | Promise<UserMessageDelivery>;
|
|
37
46
|
}
|
|
38
47
|
export interface RunStartResponse {
|
|
39
48
|
readonly run: RunStatus;
|
|
@@ -45,6 +54,15 @@ export interface RunStatusResponse {
|
|
|
45
54
|
export interface RunCancelResponse {
|
|
46
55
|
readonly run: RunStatus;
|
|
47
56
|
}
|
|
57
|
+
export interface RunMessageRequest {
|
|
58
|
+
readonly runId: string;
|
|
59
|
+
readonly text: string;
|
|
60
|
+
}
|
|
61
|
+
export interface RunMessageResponse {
|
|
62
|
+
readonly delivery: 'delivered';
|
|
63
|
+
}
|
|
64
|
+
/** Ceiling on one mid-run message, matching what a person can reasonably type or paste into a composer. */
|
|
65
|
+
export declare const RUN_MESSAGE_MAX_CHARS = 100000;
|
|
48
66
|
export interface RunListResponse {
|
|
49
67
|
readonly runs: readonly RunStatus[];
|
|
50
68
|
}
|
|
@@ -58,6 +76,13 @@ export declare const runCancelRoute: import("@jini-ai/http-kit").JsonRouteSpec<{
|
|
|
58
76
|
runId: string;
|
|
59
77
|
reason?: string;
|
|
60
78
|
}, RunCancelResponse, RunHttpDeps, undefined>;
|
|
79
|
+
/**
|
|
80
|
+
* `POST /api/runs/:runId/messages` — a message the human sent while the run is going. `202` when it
|
|
81
|
+
* reached the live agent; `409 CONFLICT` with `details.delivery` (`'unsupported'` or
|
|
82
|
+
* `'not-running'`) when it did not, which tells the client whether to interrupt the run and send the
|
|
83
|
+
* text as the next turn, or simply send it next because the run is already ending.
|
|
84
|
+
*/
|
|
85
|
+
export declare const runMessageRoute: import("@jini-ai/http-kit").JsonRouteSpec<RunMessageRequest, RunMessageResponse, RunHttpDeps, undefined>;
|
|
61
86
|
/**
|
|
62
87
|
* The core of `GET /api/runs/:runId/events` — canonical events as SSE, with Last-Event-ID
|
|
63
88
|
* reconnect support. Takes `runId`/`afterCursor` already resolved by the caller. `res` is typed
|
|
@@ -82,7 +107,7 @@ export declare function registerRunEventStream({ app, deps }: {
|
|
|
82
107
|
readonly app: Express;
|
|
83
108
|
readonly deps: RunHttpDeps;
|
|
84
109
|
}, _optional?: Record<string, never>): void;
|
|
85
|
-
/** Mounts create/status/cancel JSON endpoints and the SSE event stream as one run transport. */
|
|
110
|
+
/** Mounts create/status/cancel/message JSON endpoints and the SSE event stream as one run transport. */
|
|
86
111
|
export declare function registerRunRoutes({ app, deps, adapter }: {
|
|
87
112
|
readonly app: Express;
|
|
88
113
|
readonly deps: RunHttpDeps;
|
package/dist/http/runs.js
CHANGED
|
@@ -57,6 +57,23 @@ function parseRunCreate(input) {
|
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
|
+
/** Ceiling on one mid-run message, matching what a person can reasonably type or paste into a composer. */
|
|
61
|
+
export const RUN_MESSAGE_MAX_CHARS = 100_000;
|
|
62
|
+
function parseRunMessage(input) {
|
|
63
|
+
const parsedRunId = parseRunId(input);
|
|
64
|
+
if (!parsedRunId.ok)
|
|
65
|
+
return parsedRunId;
|
|
66
|
+
if (!isRecord(input.body))
|
|
67
|
+
return err({ error: validationError({ message: 'body must be a JSON object' }) });
|
|
68
|
+
const text = optionalString(input.body, 'text');
|
|
69
|
+
if (text === undefined || text === null) {
|
|
70
|
+
return err({ error: validationError({ message: 'text must be a non-empty string' }, { issues: [{ path: 'text', message: 'required non-empty string' }] }) });
|
|
71
|
+
}
|
|
72
|
+
if (text.length > RUN_MESSAGE_MAX_CHARS) {
|
|
73
|
+
return err({ error: validationError({ message: `text must be at most ${RUN_MESSAGE_MAX_CHARS} characters` }, { issues: [{ path: 'text', message: 'too long' }] }) });
|
|
74
|
+
}
|
|
75
|
+
return ok({ value: { runId: parsedRunId.value, text } });
|
|
76
|
+
}
|
|
60
77
|
function parseRunList(input) {
|
|
61
78
|
const value = input.query.contextRef;
|
|
62
79
|
if (value === undefined)
|
|
@@ -121,6 +138,23 @@ export const runCancelRoute = defineJsonRoute({
|
|
|
121
138
|
return ok({ value: { run: await deps.lifecycle.cancel(input) } });
|
|
122
139
|
}
|
|
123
140
|
}, { requireSameOrigin: true });
|
|
141
|
+
/**
|
|
142
|
+
* `POST /api/runs/:runId/messages` — a message the human sent while the run is going. `202` when it
|
|
143
|
+
* reached the live agent; `409 CONFLICT` with `details.delivery` (`'unsupported'` or
|
|
144
|
+
* `'not-running'`) when it did not, which tells the client whether to interrupt the run and send the
|
|
145
|
+
* text as the next turn, or simply send it next because the run is already ending.
|
|
146
|
+
*/
|
|
147
|
+
export const runMessageRoute = defineJsonRoute({
|
|
148
|
+
method: 'post', path: '/api/runs/:runId/messages', parse: parseRunMessage, handle: async ({ input, deps }) => {
|
|
149
|
+
const run = await deps.lifecycle.get({ runId: input.runId });
|
|
150
|
+
if (run === undefined)
|
|
151
|
+
return err({ error: createApiError({ code: 'NOT_FOUND', message: `run "${input.runId}" was not found` }) });
|
|
152
|
+
const delivery = deps.deliverUserMessage ? await deps.deliverUserMessage({ runId: input.runId, text: input.text }) : 'unsupported';
|
|
153
|
+
if (delivery === 'delivered')
|
|
154
|
+
return ok({ value: { delivery } });
|
|
155
|
+
return err({ error: createApiError({ code: 'CONFLICT', message: `run "${input.runId}" did not take the message` }, { details: { delivery } }) });
|
|
156
|
+
}
|
|
157
|
+
}, { requireSameOrigin: true, successStatus: 202 });
|
|
124
158
|
function sendStreamFailure(res, kind) {
|
|
125
159
|
if (kind.kind === 'unknown-run') {
|
|
126
160
|
sendRawApiError({ res, status: 404, error: createApiError({ code: 'NOT_FOUND', message: 'run was not found' }) });
|
|
@@ -210,12 +244,13 @@ export function registerRunEventStream({ app, deps }, _optional = {}) {
|
|
|
210
244
|
await handleRunEventStreamRequest({ res, runId, afterCursor: requestedAfterCursor(req), deps });
|
|
211
245
|
});
|
|
212
246
|
}
|
|
213
|
-
/** Mounts create/status/cancel JSON endpoints and the SSE event stream as one run transport. */
|
|
247
|
+
/** Mounts create/status/cancel/message JSON endpoints and the SSE event stream as one run transport. */
|
|
214
248
|
export function registerRunRoutes({ app, deps, adapter }, _optional = {}) {
|
|
215
249
|
mountJsonRoute({ app, spec: runStartRoute, deps, adapter });
|
|
216
250
|
mountJsonRoute({ app, spec: runListRoute, deps, adapter });
|
|
217
251
|
mountJsonRoute({ app, spec: runStatusRoute, deps, adapter });
|
|
218
252
|
mountJsonRoute({ app, spec: runCancelRoute, deps, adapter });
|
|
253
|
+
mountJsonRoute({ app, spec: runMessageRoute, deps, adapter });
|
|
219
254
|
registerRunEventStream({ app, deps });
|
|
220
255
|
}
|
|
221
256
|
//# sourceMappingURL=runs.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "The stateful agent daemon runtime: run lifecycle, durable event-log port, the tool-execution boundary, agent executor, terminal sessions, and routines.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -139,29 +139,29 @@
|
|
|
139
139
|
"@types/pg": "8.23.1",
|
|
140
140
|
"express": "^4.21.0",
|
|
141
141
|
"@types/express": "^4.17.21",
|
|
142
|
-
"@jini-ai/chat": "0.5.
|
|
143
|
-
"@jini-ai/
|
|
144
|
-
"@jini-ai/
|
|
145
|
-
"@jini-ai/
|
|
146
|
-
"@jini-ai/core": "^0.4.
|
|
147
|
-
"@jini-ai/
|
|
148
|
-
"@jini-ai/
|
|
142
|
+
"@jini-ai/chat": "0.5.8",
|
|
143
|
+
"@jini-ai/db": "0.3.2",
|
|
144
|
+
"@jini-ai/agent-runtime": "^0.4.5",
|
|
145
|
+
"@jini-ai/protocol": "^0.4.4",
|
|
146
|
+
"@jini-ai/core": "^0.4.3",
|
|
147
|
+
"@jini-ai/platform": "^0.5.3",
|
|
148
|
+
"@jini-ai/http-kit": "0.4.1"
|
|
149
149
|
},
|
|
150
150
|
"dependencies": {
|
|
151
|
-
"@jini-ai/core": "^0.4.
|
|
152
|
-
"@jini-ai/protocol": "^0.4.
|
|
151
|
+
"@jini-ai/core": "^0.4.3",
|
|
152
|
+
"@jini-ai/protocol": "^0.4.4"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
155
|
"node-pty": "^1.1.0",
|
|
156
|
-
"@jini-ai/db": "^0.
|
|
156
|
+
"@jini-ai/db": "^0.3.2",
|
|
157
157
|
"kysely": "^0.29.6",
|
|
158
158
|
"better-sqlite3": "^13.0.0",
|
|
159
159
|
"@electric-sql/pglite": "0.5.8",
|
|
160
160
|
"pg": "^8.23.0",
|
|
161
161
|
"@jini-ai/http-kit": "^0.4.0",
|
|
162
162
|
"express": "^4.21.0",
|
|
163
|
-
"@jini-ai/agent-runtime": "^0.4.
|
|
164
|
-
"@jini-ai/platform": "^0.5.
|
|
163
|
+
"@jini-ai/agent-runtime": "^0.4.5",
|
|
164
|
+
"@jini-ai/platform": "^0.5.3"
|
|
165
165
|
},
|
|
166
166
|
"peerDependenciesMeta": {
|
|
167
167
|
"node-pty": {
|