gestalt-mobile 0.35.4 → 0.36.0
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 +60 -3
- package/dist/client/assets/index-9XMv2BvD.js +38 -0
- package/dist/client/assets/{index-CPQBJDsg.css → index-Bxv5Z2w4.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/client/features/chat/activity-summary.js +71 -0
- package/dist/server/client/features/chat/chat-projection.js +722 -0
- package/dist/server/client/features/chat/message-store.js +30 -0
- package/dist/server/server/app.js +1 -1
- package/dist/server/server/cli.js +8 -0
- package/dist/server/server/composition.contracts.js +191 -8
- package/dist/server/server/composition.js +266 -56
- package/dist/server/server/features/autopilot/application/policy.js +2 -2
- package/dist/server/server/features/autopilot/register-routes.js +2 -2
- package/dist/server/server/features/autopilot/toggle/endpoint.js +5 -1
- package/dist/server/server/features/catalog/get-bootstrap/use-case.js +5 -3
- package/dist/server/server/features/plans/application/parse-supervised-plan.js +33 -8
- package/dist/server/server/features/sessions/get-session/endpoint.js +1 -1
- package/dist/server/server/features/sessions/list-models/endpoint.js +16 -0
- package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +8 -3
- package/dist/server/server/features/sessions/list-sessions/endpoint.js +1 -1
- package/dist/server/server/features/sessions/model/relay-session.js +2 -0
- package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +6 -2
- package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +7 -2
- package/dist/server/server/features/sessions/register-routes.js +8 -3
- package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
- package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
- package/dist/server/server/features/sessions/start-session/request.js +1 -0
- package/dist/server/server/features/sessions/start-session/use-case.js +21 -7
- package/dist/server/server/features/skills/list-available/endpoint.js +4 -2
- package/dist/server/server/platform/catalog/provider-model-catalog.js +15 -0
- package/dist/server/server/platform/kimi/kimi-activity-facts.js +88 -0
- package/dist/server/server/platform/kimi/kimi-errors.js +30 -0
- package/dist/server/server/platform/kimi/kimi-model-catalog.js +63 -0
- package/dist/server/server/platform/kimi/kimi-normalizer.js +196 -0
- package/dist/server/server/platform/kimi/kimi-recent-session-lister.js +68 -0
- package/dist/server/server/platform/kimi/kimi-server-request.js +143 -0
- package/dist/server/server/platform/kimi/kimi-session-runtime.js +653 -0
- package/dist/server/server/platform/kimi/kimi-share-dir.js +65 -0
- package/dist/server/server/platform/kimi/kimi-skill-catalog.js +79 -0
- package/dist/server/server/platform/kimi/kimi-skills-materializer.js +41 -0
- package/dist/server/server/platform/kimi/kimi-web-client.js +63 -0
- package/dist/server/server/platform/kimi/kimi-web-server-manager.js +215 -0
- package/dist/server/server/platform/kimi/kimi-ws-client.js +154 -0
- package/dist/server/server/platform/persistence/migrate.js +2 -0
- package/dist/server/server/platform/persistence/sqlite-session-repository.js +3 -2
- package/dist/server/server/platform/skills/cached-skill-catalog.js +0 -0
- package/dist/server/shared/contracts/llm-provider.js +6 -0
- package/package.json +4 -3
- package/dist/client/assets/index-SZM7lpix.js +0 -38
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
const paramsSchema = z.object({ provider: z.enum(['codex', 'kimi']) });
|
|
8
|
+
/** Resolves the selected provider's current model catalog for new-session setup. */
|
|
9
|
+
export function registerListSessionModels(app, models) {
|
|
10
|
+
app.post('/api/session-models/:provider', async (request, reply) => {
|
|
11
|
+
const parsed = paramsSchema.safeParse(request.params);
|
|
12
|
+
if (!parsed.success)
|
|
13
|
+
return reply.code(400).send({ code: 'PROVIDER_INVALID' });
|
|
14
|
+
return { models: await models.list(parsed.data.provider) };
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -7,18 +7,23 @@ import { buildResumeCommand } from '../application/resume-command.js';
|
|
|
7
7
|
export function registerListRecentThreads(app, deps) {
|
|
8
8
|
app.get('/api/sessions/recent-threads', async () => {
|
|
9
9
|
const threads = await deps.list();
|
|
10
|
-
return threads.map(({ id, cwd, recencyAt }) => {
|
|
11
|
-
const metadata = deps.metadata?.(id);
|
|
10
|
+
return threads.map(({ id, cwd, recencyAt, provider }) => {
|
|
11
|
+
const metadata = deps.metadata?.(id, provider ?? 'codex');
|
|
12
12
|
return {
|
|
13
13
|
id,
|
|
14
14
|
cwd,
|
|
15
15
|
recencyAt,
|
|
16
|
+
...(provider ? { provider } : {}),
|
|
16
17
|
...(metadata?.model === undefined ? {} : { model: metadata.model }),
|
|
17
18
|
...(metadata?.skillProfile === undefined ? {} : { skillProfile: metadata.skillProfile }),
|
|
18
19
|
...(metadata?.orgPlanFilename === undefined
|
|
19
20
|
? {}
|
|
20
21
|
: { orgPlanFilename: metadata.orgPlanFilename }),
|
|
21
|
-
|
|
22
|
+
// The CLI resume command is codex-specific; kimi threads open in the
|
|
23
|
+
// relay without one.
|
|
24
|
+
...(provider === 'kimi'
|
|
25
|
+
? {}
|
|
26
|
+
: { resumeCommand: buildResumeCommand({ threadId: id, workspacePath: cwd }) }),
|
|
22
27
|
};
|
|
23
28
|
});
|
|
24
29
|
});
|
|
@@ -24,7 +24,7 @@ export function registerListSessions(app, deps) {
|
|
|
24
24
|
pendingAttention: Boolean(session.pendingInteractions?.length),
|
|
25
25
|
observedAt: new Date().toISOString(),
|
|
26
26
|
}),
|
|
27
|
-
resumeCommand: session.threadId ? buildResumeCommand(session) : null,
|
|
27
|
+
resumeCommand: session.threadId && session.provider !== 'kimi' ? buildResumeCommand(session) : null,
|
|
28
28
|
};
|
|
29
29
|
}));
|
|
30
30
|
}
|
|
@@ -51,6 +51,7 @@ export class RelaySession {
|
|
|
51
51
|
id: sessionId(input.id),
|
|
52
52
|
workspaceId: workspaceId(input.workspaceId),
|
|
53
53
|
workspacePath: workspacePath(input.workspacePath),
|
|
54
|
+
provider: input.provider,
|
|
54
55
|
profile: profileName(input.profile),
|
|
55
56
|
...(input.model === undefined ? {} : { model: input.model }),
|
|
56
57
|
...(input.branch === undefined ? {} : { branch: input.branch }),
|
|
@@ -72,6 +73,7 @@ export class RelaySession {
|
|
|
72
73
|
id: sessionId(input.id),
|
|
73
74
|
workspaceId: workspaceId(input.workspaceId),
|
|
74
75
|
workspacePath: workspacePath(input.workspacePath),
|
|
76
|
+
provider: input.provider,
|
|
75
77
|
profile: profileName(input.profile),
|
|
76
78
|
...(input.executionPolicy === undefined
|
|
77
79
|
? {}
|
|
@@ -8,13 +8,17 @@ import { RecentThreadHistoryUnavailable } from './use-case.js';
|
|
|
8
8
|
const requestSchema = z.object({
|
|
9
9
|
threadId: z.string().min(1),
|
|
10
10
|
cwd: z.string().startsWith('/'),
|
|
11
|
+
// Requests written before provider support remain Codex-compatible.
|
|
12
|
+
provider: z.enum(['codex', 'kimi']).optional().default('codex'),
|
|
11
13
|
});
|
|
12
14
|
export function registerPromoteRecentThread(app, deps) {
|
|
13
15
|
app.post('/api/sessions/recent-threads/open', { bodyLimit: 8 * 1024 }, async (request, reply) => {
|
|
14
16
|
const parsed = requestSchema.safeParse(request.body);
|
|
15
17
|
if (!parsed.success)
|
|
16
18
|
return reply.code(400).send({ code: 'INVALID_RECENT_THREAD' });
|
|
17
|
-
const thread = (await deps.list()).find((candidate) => candidate.id === parsed.data.threadId &&
|
|
19
|
+
const thread = (await deps.list()).find((candidate) => candidate.id === parsed.data.threadId &&
|
|
20
|
+
candidate.cwd === parsed.data.cwd &&
|
|
21
|
+
(candidate.provider ?? 'codex') === parsed.data.provider);
|
|
18
22
|
if (!thread)
|
|
19
23
|
return reply.code(404).send({ code: 'RECENT_THREAD_NOT_FOUND' });
|
|
20
24
|
try {
|
|
@@ -27,7 +31,7 @@ export function registerPromoteRecentThread(app, deps) {
|
|
|
27
31
|
? 'RECENT_THREAD_HISTORY_UNAVAILABLE'
|
|
28
32
|
: 'RECENT_THREAD_OPEN_FAILED',
|
|
29
33
|
detail: historyUnavailable
|
|
30
|
-
? 'The selected thread history is currently unavailable. Retry after
|
|
34
|
+
? 'The selected thread history is currently unavailable. Retry after its provider is available.'
|
|
31
35
|
: 'The selected thread could not be opened. Retry shortly.',
|
|
32
36
|
});
|
|
33
37
|
}
|
|
@@ -11,7 +11,8 @@ export class RecentThreadHistoryUnavailable extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
const inFlightPromotions = new Map();
|
|
13
13
|
export async function promoteRecentThread(thread, deps) {
|
|
14
|
-
const
|
|
14
|
+
const provider = thread.provider ?? 'codex';
|
|
15
|
+
const key = `${provider}:${thread.profile}:${thread.cwd}:${thread.id}`;
|
|
15
16
|
const ongoing = inFlightPromotions.get(key);
|
|
16
17
|
if (ongoing)
|
|
17
18
|
return ongoing;
|
|
@@ -26,7 +27,10 @@ export async function promoteRecentThread(thread, deps) {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
async function promote(thread, deps) {
|
|
29
|
-
const
|
|
30
|
+
const provider = thread.provider ?? 'codex';
|
|
31
|
+
const existing = deps
|
|
32
|
+
.list()
|
|
33
|
+
.find((session) => session.threadId === thread.id && (session.provider ?? 'codex') === provider);
|
|
30
34
|
if (existing) {
|
|
31
35
|
const executionPolicy = existing.executionPolicy
|
|
32
36
|
? undefined
|
|
@@ -51,6 +55,7 @@ async function promote(thread, deps) {
|
|
|
51
55
|
id: deps.createId(),
|
|
52
56
|
workspaceId: thread.cwd,
|
|
53
57
|
workspacePath: thread.cwd,
|
|
58
|
+
provider,
|
|
54
59
|
profile: thread.profile,
|
|
55
60
|
threadId: thread.id,
|
|
56
61
|
...(executionPolicy === undefined ? {} : { executionPolicy }),
|
|
@@ -17,6 +17,7 @@ import { registerReleaseSession } from './release-session/endpoint.js';
|
|
|
17
17
|
import { registerRestoreSession } from './restore-session/endpoint.js';
|
|
18
18
|
import { registerRespondInteraction } from './respond-interaction/endpoint.js';
|
|
19
19
|
import { registerSelectModel } from './select-model/endpoint.js';
|
|
20
|
+
import { registerListSessionModels } from './list-models/endpoint.js';
|
|
20
21
|
import { registerSessionEvents } from './session-events/endpoint.js';
|
|
21
22
|
import { registerStartSession } from './start-session/endpoint.js';
|
|
22
23
|
import { registerStartTurn } from './start-turn/endpoint.js';
|
|
@@ -27,8 +28,10 @@ export function registerSessionRoutes(app, deps) {
|
|
|
27
28
|
if (deps.recentThreads) {
|
|
28
29
|
registerListRecentThreads(app, {
|
|
29
30
|
...deps.recentThreads,
|
|
30
|
-
metadata: (threadId) => {
|
|
31
|
-
const session = sessions
|
|
31
|
+
metadata: (threadId, provider) => {
|
|
32
|
+
const session = sessions
|
|
33
|
+
?.list?.()
|
|
34
|
+
.find((candidate) => candidate.threadId === threadId && (candidate.provider ?? 'codex') === provider);
|
|
32
35
|
if (!session)
|
|
33
36
|
return null;
|
|
34
37
|
return {
|
|
@@ -44,6 +47,8 @@ export function registerSessionRoutes(app, deps) {
|
|
|
44
47
|
});
|
|
45
48
|
}
|
|
46
49
|
if (sessions) {
|
|
50
|
+
if (sessions.sessionModels)
|
|
51
|
+
registerListSessionModels(app, sessions.sessionModels);
|
|
47
52
|
registerStartSession(app, {
|
|
48
53
|
...sessions,
|
|
49
54
|
reportFailure: (operation, error) => deps.logger.error(`${operation} failed: ${safeErrorLabel(error)}`),
|
|
@@ -84,7 +89,7 @@ export function registerSessionRoutes(app, deps) {
|
|
|
84
89
|
if (sessions.models)
|
|
85
90
|
registerSelectModel(app, {
|
|
86
91
|
find: sessions.find,
|
|
87
|
-
models: () => sessions.models.list(),
|
|
92
|
+
models: (provider) => sessions.models.list(provider),
|
|
88
93
|
now: sessions.now,
|
|
89
94
|
save: sessions.save,
|
|
90
95
|
});
|
|
@@ -18,7 +18,7 @@ export function registerRespondInteraction(app, deps) {
|
|
|
18
18
|
return reply.code(409).send({ code: 'INTERACTION_NOT_PENDING' });
|
|
19
19
|
if (deps.validate && !deps.validate(id, requestId, value))
|
|
20
20
|
return reply.code(400).send({ code: 'INTERACTION_RESPONSE_INVALID' });
|
|
21
|
-
const delivery = deps.reply(id, requestId, value);
|
|
21
|
+
const delivery = await deps.reply(id, requestId, value);
|
|
22
22
|
if (delivery === false || delivery === 'unavailable')
|
|
23
23
|
return reply.code(409).send({ code: 'INTERACTION_DELIVERY_UNAVAILABLE' });
|
|
24
24
|
if (delivery === 'cleared') {
|
|
@@ -12,7 +12,7 @@ export function registerSelectModel(app, deps) {
|
|
|
12
12
|
const model = request.body.model;
|
|
13
13
|
if (typeof model !== 'string')
|
|
14
14
|
return reply.code(400).send({ code: 'MODEL_REQUIRED' });
|
|
15
|
-
if (!(await deps.models()).includes(model))
|
|
15
|
+
if (!(await deps.models(session.provider)).includes(model))
|
|
16
16
|
return reply.code(400).send({ code: 'MODEL_UNAVAILABLE' });
|
|
17
17
|
if (session.state !== 'ready')
|
|
18
18
|
return reply.code(409).send({ code: 'SESSION_NOT_READY' });
|
|
@@ -8,15 +8,28 @@ import { DEFAULT_SESSION_MODEL } from '../application/start-settings.js';
|
|
|
8
8
|
import { reconcileSkillSelectionSnapshot, } from '../../skills/model/skill-profile.js';
|
|
9
9
|
import { SkillProfileError } from '../../skills/model/errors.js';
|
|
10
10
|
export async function startSession(input, deps) {
|
|
11
|
-
|
|
11
|
+
// Codex launcher profiles select the app-server skill directory. Kimi owns
|
|
12
|
+
// its isolated profile lifecycle, so it must not require a Codex launcher
|
|
13
|
+
// profile before its own catalog/runtime can be selected.
|
|
12
14
|
const [workspace] = await Promise.all([
|
|
13
15
|
deps.workspaces.resolve(input.workspaceId),
|
|
14
|
-
deps.profiles.require(input.profile),
|
|
16
|
+
...(input.provider === 'codex' ? [deps.profiles.require(input.profile)] : []),
|
|
15
17
|
]);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
let model = input.model;
|
|
19
|
+
const modelCatalog = deps.sessionModels ?? deps.models;
|
|
20
|
+
if (modelCatalog) {
|
|
21
|
+
const models = await modelCatalog.list(input.provider);
|
|
22
|
+
// An omitted model resolves to the provider's own default: the relay-wide
|
|
23
|
+
// default when that provider serves it, otherwise the provider's first
|
|
24
|
+
// available model, so a kimi request never inherits the codex default.
|
|
25
|
+
model ??= models.includes(DEFAULT_SESSION_MODEL)
|
|
26
|
+
? DEFAULT_SESSION_MODEL
|
|
27
|
+
: (models[0] ?? DEFAULT_SESSION_MODEL);
|
|
18
28
|
if (!models.includes(model))
|
|
19
|
-
throw new Error('CODEX_MODEL_UNAVAILABLE');
|
|
29
|
+
throw new Error(input.provider === 'codex' ? 'CODEX_MODEL_UNAVAILABLE' : 'KIMI_MODEL_UNAVAILABLE');
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
model ??= DEFAULT_SESSION_MODEL;
|
|
20
33
|
}
|
|
21
34
|
const selectedProfile = input.skillProfile
|
|
22
35
|
? await deps.skillProfiles.readGlobalProfile(input.skillProfile)
|
|
@@ -25,7 +38,7 @@ export async function startSession(input, deps) {
|
|
|
25
38
|
throw new SkillProfileError('UNKNOWN_SKILL_PROFILE', 'The selected skill profile does not exist.');
|
|
26
39
|
const [projectProfile, catalog] = await Promise.all([
|
|
27
40
|
deps.skillProfiles.readWorkspaceDefault(workspace.realPath),
|
|
28
|
-
deps.skillCatalog(input.profile).list(workspace.realPath),
|
|
41
|
+
deps.skillCatalog(input.provider, input.profile).list(workspace.realPath),
|
|
29
42
|
]);
|
|
30
43
|
const sourceProfile = selectedProfile ?? projectProfile;
|
|
31
44
|
const reconciledSkills = reconcileSkillSelectionSnapshot(catalog.skills, sourceProfile?.skills);
|
|
@@ -46,6 +59,7 @@ export async function startSession(input, deps) {
|
|
|
46
59
|
id: deps.createId(),
|
|
47
60
|
workspaceId: workspace.id,
|
|
48
61
|
workspacePath: workspace.realPath,
|
|
62
|
+
provider: input.provider,
|
|
49
63
|
profile: input.profile,
|
|
50
64
|
model,
|
|
51
65
|
...(branch ? { branch } : {}),
|
|
@@ -57,7 +71,7 @@ export async function startSession(input, deps) {
|
|
|
57
71
|
deps.save(session);
|
|
58
72
|
if (!deps.activate)
|
|
59
73
|
return session;
|
|
60
|
-
const active = await deps.activate(session, {});
|
|
74
|
+
const active = await deps.activate(session, { provider: session.provider });
|
|
61
75
|
deps.save(active);
|
|
62
76
|
return active;
|
|
63
77
|
}
|
|
@@ -11,6 +11,7 @@ const querySchema = z
|
|
|
11
11
|
.object({
|
|
12
12
|
workspaceId: z.string().min(1),
|
|
13
13
|
profile: z.string().min(1),
|
|
14
|
+
provider: z.enum(['codex', 'kimi']).optional(),
|
|
14
15
|
refresh: z.enum(['true']).optional(),
|
|
15
16
|
})
|
|
16
17
|
.strict();
|
|
@@ -28,10 +29,11 @@ export function registerListAvailableSkills(app, deps) {
|
|
|
28
29
|
deps.workspaces.resolve(parsed.data.workspaceId),
|
|
29
30
|
deps.profiles.require(parsed.data.profile),
|
|
30
31
|
]);
|
|
32
|
+
const provider = parsed.data.provider ?? 'codex';
|
|
31
33
|
const [discovered, project] = await Promise.all([
|
|
32
34
|
parsed.data.refresh
|
|
33
|
-
? deps.catalog.refresh(profile.name, workspace.realPath)
|
|
34
|
-
: deps.catalog.list(profile.name, workspace.realPath),
|
|
35
|
+
? deps.catalog.refresh(provider, profile.name, workspace.realPath)
|
|
36
|
+
: deps.catalog.list(provider, profile.name, workspace.realPath),
|
|
35
37
|
deps.selections.readWorkspaceDefault(workspace.realPath),
|
|
36
38
|
]);
|
|
37
39
|
const skills = applySkillSelectionSnapshot(discovered.skills, project?.skills);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
/** Routes each provider's model query to that provider's own catalog adapter. */
|
|
7
|
+
export class ProviderModelCatalog {
|
|
8
|
+
catalogs;
|
|
9
|
+
constructor(catalogs) {
|
|
10
|
+
this.catalogs = catalogs;
|
|
11
|
+
}
|
|
12
|
+
list(provider) {
|
|
13
|
+
return this.catalogs[provider].list();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Decodes bounded activity evidence from kimi web events. Turn lifecycle is
|
|
8
|
+
* prompt-based; `agent.status.updated` supplies context usage and
|
|
9
|
+
* awaiting-human transitions (kimi 2.0.2 has no dedicated interaction events —
|
|
10
|
+
* pending approvals/questions arrive through status changes plus REST lists).
|
|
11
|
+
*/
|
|
12
|
+
export function decodeKimiActivityFacts(sessionId, occurredAt, event, context) {
|
|
13
|
+
const payload = record(event.payload);
|
|
14
|
+
if (!payload || typeof payload.type !== 'string')
|
|
15
|
+
return [];
|
|
16
|
+
switch (payload.type) {
|
|
17
|
+
case 'turn.started': {
|
|
18
|
+
const agentId = safeId(payload.agentId);
|
|
19
|
+
const turnNumber = safeTurn(payload.turnId);
|
|
20
|
+
if (!agentId || turnNumber === null)
|
|
21
|
+
return [];
|
|
22
|
+
const turnId = context.resolveTurnId(agentId, turnNumber);
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
sessionId,
|
|
26
|
+
occurredAt,
|
|
27
|
+
kind: 'turnStarted',
|
|
28
|
+
...(event.session_id ? { threadId: event.session_id } : {}),
|
|
29
|
+
...(turnId ? { turnId } : {}),
|
|
30
|
+
...(context.isChildAgent(agentId) ? { childThreadId: agentId } : {}),
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
case 'prompt.completed':
|
|
35
|
+
case 'prompt.aborted': {
|
|
36
|
+
const agentId = safeId(payload.agentId);
|
|
37
|
+
const promptId = safeId(payload.promptId);
|
|
38
|
+
if (!agentId || !promptId || context.isChildAgent(agentId))
|
|
39
|
+
return [];
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
sessionId,
|
|
43
|
+
occurredAt,
|
|
44
|
+
kind: 'turnCompleted',
|
|
45
|
+
...(event.session_id ? { threadId: event.session_id } : {}),
|
|
46
|
+
turnId: promptId,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
case 'agent.status.updated': {
|
|
51
|
+
const status = typeof payload.status === 'string' ? payload.status : null;
|
|
52
|
+
const facts = [];
|
|
53
|
+
const usage = typeof payload.contextUsage === 'number' ? payload.contextUsage : null;
|
|
54
|
+
if (usage !== null && usage >= 0) {
|
|
55
|
+
facts.push({
|
|
56
|
+
sessionId,
|
|
57
|
+
occurredAt,
|
|
58
|
+
kind: 'contextUsage',
|
|
59
|
+
...(event.session_id ? { threadId: event.session_id } : {}),
|
|
60
|
+
contextUsedPercent: usage <= 1 ? Math.round(usage * 100) : Math.min(100, Math.round(usage)),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (status === 'awaiting_approval' || status === 'awaiting_question') {
|
|
64
|
+
facts.push({
|
|
65
|
+
sessionId,
|
|
66
|
+
occurredAt,
|
|
67
|
+
kind: 'interactionPending',
|
|
68
|
+
...(event.session_id ? { threadId: event.session_id } : {}),
|
|
69
|
+
attentionReason: status === 'awaiting_approval' ? 'permissionRequired' : 'materialAmbiguity',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return facts;
|
|
73
|
+
}
|
|
74
|
+
default:
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function record(value) {
|
|
79
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
80
|
+
? value
|
|
81
|
+
: null;
|
|
82
|
+
}
|
|
83
|
+
function safeId(value) {
|
|
84
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 256 ? value : null;
|
|
85
|
+
}
|
|
86
|
+
function safeTurn(value) {
|
|
87
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
88
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Error surfaced by the kimi web client. `code` is the protocol business code
|
|
8
|
+
* (0 means success on the wire, so a thrown error always has code !== 0);
|
|
9
|
+
* HTTP-level failures are normalized to code -1 with the transport message.
|
|
10
|
+
*/
|
|
11
|
+
export class KimiWebError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
requestId;
|
|
14
|
+
constructor(code, message, requestId) {
|
|
15
|
+
super(boundMessage(message));
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.requestId = requestId;
|
|
18
|
+
this.name = 'KimiWebError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function isKimiWebError(error) {
|
|
22
|
+
return error instanceof KimiWebError;
|
|
23
|
+
}
|
|
24
|
+
/** Caps and redacts wire messages before they enter logs or client payloads. */
|
|
25
|
+
export function boundMessage(message) {
|
|
26
|
+
return message
|
|
27
|
+
.replace(/((?:authorization|token|api[_ -]?key|password)\s*[:=]\s*)\S+/gi, '$1[REDACTED]')
|
|
28
|
+
.replace(/(authentication failed:\s*).*/i, '$1[REDACTED]')
|
|
29
|
+
.slice(0, 256);
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
const MODELS_TIMEOUT_MS = 5_000;
|
|
7
|
+
/**
|
|
8
|
+
* Lists kimi model aliases from a running gestalt-owned kimi web server
|
|
9
|
+
* (`GET /api/v1/models`) when a Kimi profile server is already running.
|
|
10
|
+
* Discovery is deliberately read-only: bootstrap must not spawn or wait for
|
|
11
|
+
* Kimi on behalf of an otherwise ordinary Codex client. Explicit Kimi session
|
|
12
|
+
* activation owns server startup; any discovery failure degrades to an empty
|
|
13
|
+
* list, matching the catalog contract.
|
|
14
|
+
*/
|
|
15
|
+
export class KimiModelCatalog {
|
|
16
|
+
servers;
|
|
17
|
+
available;
|
|
18
|
+
constructor(servers, available) {
|
|
19
|
+
this.servers = servers;
|
|
20
|
+
this.available = available;
|
|
21
|
+
}
|
|
22
|
+
async list() {
|
|
23
|
+
if (!this.available || !this.servers)
|
|
24
|
+
return [];
|
|
25
|
+
try {
|
|
26
|
+
const handle = this.servers.list()[0];
|
|
27
|
+
if (!handle)
|
|
28
|
+
return [];
|
|
29
|
+
return await this.read(handle);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Explicit Kimi session setup may start its isolated default server. */
|
|
36
|
+
async listForSession() {
|
|
37
|
+
if (!this.available || !this.servers)
|
|
38
|
+
return [];
|
|
39
|
+
try {
|
|
40
|
+
return await this.read(await this.servers.ensure('default', []));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async read(handle) {
|
|
47
|
+
const data = await withTimeout(handle.client.get('/api/v1/models'), MODELS_TIMEOUT_MS, 'kimi model list timed out');
|
|
48
|
+
const items = Array.isArray(data?.items)
|
|
49
|
+
? data.items
|
|
50
|
+
: [];
|
|
51
|
+
return [
|
|
52
|
+
...new Set(items.flatMap((item) => typeof item.model === 'string' && item.model.length > 0 && item.model.length <= 128
|
|
53
|
+
? [item.model]
|
|
54
|
+
: [])),
|
|
55
|
+
].sort();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function withTimeout(promise, timeoutMs, message) {
|
|
59
|
+
return Promise.race([
|
|
60
|
+
promise,
|
|
61
|
+
new Promise((_resolve, reject) => setTimeout(() => reject(new Error(message)), timeoutMs).unref()),
|
|
62
|
+
]);
|
|
63
|
+
}
|