@kortyx/agent 0.2.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/CHANGELOG.md +8 -0
- package/dist/chat/create-agent.d.ts +16 -0
- package/dist/chat/create-agent.d.ts.map +1 -0
- package/dist/chat/create-agent.js +61 -0
- package/dist/chat/process-chat.d.ts +25 -0
- package/dist/chat/process-chat.d.ts.map +1 -0
- package/dist/chat/process-chat.js +69 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/interrupt/resume-handler.d.ts +27 -0
- package/dist/interrupt/resume-handler.d.ts.map +1 -0
- package/dist/interrupt/resume-handler.js +89 -0
- package/dist/orchestrator.d.ts +20 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +391 -0
- package/dist/stream/transform-graph-stream-for-ui.d.ts +10 -0
- package/dist/stream/transform-graph-stream-for-ui.d.ts.map +1 -0
- package/dist/stream/transform-graph-stream-for-ui.js +194 -0
- package/dist/types/chat-message.d.ts +8 -0
- package/dist/types/chat-message.d.ts.map +1 -0
- package/dist/types/chat-message.js +2 -0
- package/dist/utils/extract-latest-message.d.ts +3 -0
- package/dist/utils/extract-latest-message.d.ts.map +1 -0
- package/dist/utils/extract-latest-message.js +14 -0
- package/package.json +36 -0
- package/src/chat/create-agent.ts +97 -0
- package/src/chat/process-chat.ts +132 -0
- package/src/index.ts +22 -0
- package/src/interrupt/resume-handler.ts +146 -0
- package/src/orchestrator.ts +532 -0
- package/src/stream/transform-graph-stream-for-ui.ts +245 -0
- package/src/types/chat-message.ts +7 -0
- package/src/utils/extract-latest-message.ts +13 -0
- package/tsconfig.build.json +21 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { KortyxConfig, WorkflowRegistry } from "@kortyx/runtime";
|
|
2
|
+
import { createFileWorkflowRegistry, loadKortyxConfig } from "@kortyx/runtime";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import type { SelectWorkflowFn } from "../orchestrator";
|
|
5
|
+
import type { ChatMessage } from "../types/chat-message";
|
|
6
|
+
import type { ProcessChatArgs } from "./process-chat";
|
|
7
|
+
import { processChat } from "./process-chat";
|
|
8
|
+
|
|
9
|
+
export interface CreateAgentArgs<
|
|
10
|
+
Config extends Record<string, unknown>,
|
|
11
|
+
Options,
|
|
12
|
+
> extends Omit<
|
|
13
|
+
ProcessChatArgs<Config, Options>,
|
|
14
|
+
"messages" | "options" | "selectWorkflow" | "workflowRegistry"
|
|
15
|
+
> {
|
|
16
|
+
workflowsDir?: string;
|
|
17
|
+
workflowRegistry?: WorkflowRegistry;
|
|
18
|
+
selectWorkflow?: SelectWorkflowFn;
|
|
19
|
+
fallbackWorkflowId?: string;
|
|
20
|
+
config?: KortyxConfig;
|
|
21
|
+
configPath?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createAgent<
|
|
25
|
+
Config extends Record<string, unknown>,
|
|
26
|
+
Options = unknown,
|
|
27
|
+
>(args: CreateAgentArgs<Config, Options>) {
|
|
28
|
+
const {
|
|
29
|
+
workflowsDir,
|
|
30
|
+
workflowRegistry,
|
|
31
|
+
selectWorkflow,
|
|
32
|
+
fallbackWorkflowId,
|
|
33
|
+
config,
|
|
34
|
+
configPath,
|
|
35
|
+
...baseArgs
|
|
36
|
+
} = args;
|
|
37
|
+
|
|
38
|
+
const resolvedCwd = process.cwd();
|
|
39
|
+
const registryPromise: Promise<WorkflowRegistry | undefined> = (async () => {
|
|
40
|
+
if (workflowRegistry) return workflowRegistry;
|
|
41
|
+
if (workflowsDir) {
|
|
42
|
+
return createFileWorkflowRegistry({
|
|
43
|
+
workflowsDir,
|
|
44
|
+
fallbackId: fallbackWorkflowId ?? "general-chat",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const loadConfigArgs = {
|
|
49
|
+
cwd: resolvedCwd,
|
|
50
|
+
...(configPath ? { configPath } : {}),
|
|
51
|
+
} as const;
|
|
52
|
+
const loadedConfig = config ?? (await loadKortyxConfig(loadConfigArgs));
|
|
53
|
+
const resolvedWorkflowsDir =
|
|
54
|
+
loadedConfig?.workflowsDir ?? resolve(resolvedCwd, "src", "workflows");
|
|
55
|
+
const registryOptions = {
|
|
56
|
+
workflowsDir: resolvedWorkflowsDir,
|
|
57
|
+
fallbackId:
|
|
58
|
+
loadedConfig?.fallbackWorkflowId ??
|
|
59
|
+
fallbackWorkflowId ??
|
|
60
|
+
"general-chat",
|
|
61
|
+
...(loadedConfig?.registry?.cache !== undefined
|
|
62
|
+
? { cache: loadedConfig.registry.cache }
|
|
63
|
+
: {}),
|
|
64
|
+
...(loadedConfig?.registry?.extensions
|
|
65
|
+
? { extensions: loadedConfig.registry.extensions }
|
|
66
|
+
: {}),
|
|
67
|
+
} as const;
|
|
68
|
+
return createFileWorkflowRegistry(registryOptions);
|
|
69
|
+
})();
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
processChat: async (messages: ChatMessage[], options?: Options) => {
|
|
73
|
+
if (selectWorkflow) {
|
|
74
|
+
return processChat({
|
|
75
|
+
...(baseArgs as ProcessChatArgs<Config, Options>),
|
|
76
|
+
messages,
|
|
77
|
+
options,
|
|
78
|
+
selectWorkflow,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const registry = await registryPromise;
|
|
83
|
+
if (!registry) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"createAgent requires workflowsDir, workflowRegistry, or selectWorkflow.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return processChat({
|
|
90
|
+
...(baseArgs as ProcessChatArgs<Config, Options>),
|
|
91
|
+
messages,
|
|
92
|
+
options,
|
|
93
|
+
workflowRegistry: registry,
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { GraphState, MemoryEnvelope } from "@kortyx/core";
|
|
2
|
+
import type { MemoryAdapter } from "@kortyx/memory";
|
|
3
|
+
import type { GetProviderFn } from "@kortyx/providers";
|
|
4
|
+
import type { WorkflowRegistry } from "@kortyx/runtime";
|
|
5
|
+
import { buildInitialGraphState, createLangGraph } from "@kortyx/runtime";
|
|
6
|
+
import { createStreamResponse, type StreamChunk } from "@kortyx/stream";
|
|
7
|
+
import type { ApplyResumeSelection } from "../interrupt/resume-handler";
|
|
8
|
+
import { tryPrepareResumeStream } from "../interrupt/resume-handler";
|
|
9
|
+
import type { SaveMemoryFn, SelectWorkflowFn } from "../orchestrator";
|
|
10
|
+
import { orchestrateGraphStream } from "../orchestrator";
|
|
11
|
+
import type { ChatMessage } from "../types/chat-message";
|
|
12
|
+
import { extractLatestUserMessage } from "../utils/extract-latest-message";
|
|
13
|
+
|
|
14
|
+
type InitializeProvidersFn<Config> = (
|
|
15
|
+
aiConfig: Config extends { ai: infer A } ? A : unknown,
|
|
16
|
+
) => void;
|
|
17
|
+
|
|
18
|
+
export interface ProcessChatArgs<
|
|
19
|
+
Config extends Record<string, unknown>,
|
|
20
|
+
Options,
|
|
21
|
+
> {
|
|
22
|
+
messages: ChatMessage[];
|
|
23
|
+
options?: Options | undefined;
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
defaultWorkflowId?: string;
|
|
26
|
+
loadRuntimeConfig: (options?: Options) => Config | Promise<Config>;
|
|
27
|
+
selectWorkflow?: SelectWorkflowFn;
|
|
28
|
+
workflowRegistry?: WorkflowRegistry;
|
|
29
|
+
getProvider: GetProviderFn;
|
|
30
|
+
initializeProviders?: InitializeProvidersFn<Config>;
|
|
31
|
+
memoryAdapter: MemoryAdapter;
|
|
32
|
+
applyResumeSelection?: ApplyResumeSelection;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function processChat<
|
|
36
|
+
Config extends Record<string, unknown>,
|
|
37
|
+
Options = unknown,
|
|
38
|
+
>({
|
|
39
|
+
messages,
|
|
40
|
+
options,
|
|
41
|
+
sessionId,
|
|
42
|
+
defaultWorkflowId,
|
|
43
|
+
loadRuntimeConfig,
|
|
44
|
+
selectWorkflow,
|
|
45
|
+
workflowRegistry,
|
|
46
|
+
getProvider,
|
|
47
|
+
initializeProviders,
|
|
48
|
+
memoryAdapter,
|
|
49
|
+
applyResumeSelection,
|
|
50
|
+
}: ProcessChatArgs<Config, Options>): Promise<Response> {
|
|
51
|
+
// Load runtime configuration (API keys, environment, etc.)
|
|
52
|
+
const config = await loadRuntimeConfig(options);
|
|
53
|
+
if (initializeProviders) {
|
|
54
|
+
initializeProviders((config as any)?.ai);
|
|
55
|
+
}
|
|
56
|
+
const runtimeConfig = {
|
|
57
|
+
...config,
|
|
58
|
+
getProvider,
|
|
59
|
+
...(memoryAdapter ? { memoryAdapter } : {}),
|
|
60
|
+
} as Record<string, unknown>;
|
|
61
|
+
|
|
62
|
+
const workflowSelector: SelectWorkflowFn | null =
|
|
63
|
+
selectWorkflow ??
|
|
64
|
+
(workflowRegistry ? (id) => workflowRegistry.select(id) : null);
|
|
65
|
+
if (!workflowSelector) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"processChat requires selectWorkflow or workflowRegistry to resolve workflows.",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extract session + input
|
|
72
|
+
const fallbackSessionId = (options as { sessionId?: string } | undefined)
|
|
73
|
+
?.sessionId;
|
|
74
|
+
const resolvedSessionId =
|
|
75
|
+
sessionId || fallbackSessionId || "anonymous-session";
|
|
76
|
+
const last = messages[messages.length - 1];
|
|
77
|
+
const input = extractLatestUserMessage(messages);
|
|
78
|
+
|
|
79
|
+
// Load conversation memory (Redis memory)
|
|
80
|
+
const previousMessages = messages.slice(0, -1);
|
|
81
|
+
const storedState = await memoryAdapter.load(resolvedSessionId);
|
|
82
|
+
const memory: MemoryEnvelope = {
|
|
83
|
+
...(storedState?.memory ?? {}),
|
|
84
|
+
...(previousMessages.length > 0 && {
|
|
85
|
+
conversationMessages: previousMessages,
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Base state (LLM input, messages, memory)
|
|
90
|
+
const baseState = await buildInitialGraphState({
|
|
91
|
+
input,
|
|
92
|
+
config: runtimeConfig,
|
|
93
|
+
memory,
|
|
94
|
+
...(defaultWorkflowId ? { defaultWorkflowId } : {}),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const saveMemory: SaveMemoryFn = async (
|
|
98
|
+
activeSessionId: string,
|
|
99
|
+
state: GraphState,
|
|
100
|
+
) => {
|
|
101
|
+
await memoryAdapter.save(activeSessionId, state);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// If this is a resume request, continue from pending snapshot and skip workflow determination
|
|
105
|
+
const resumeStream = await tryPrepareResumeStream({
|
|
106
|
+
lastMessage: last,
|
|
107
|
+
sessionId: resolvedSessionId,
|
|
108
|
+
config: runtimeConfig,
|
|
109
|
+
saveMemory,
|
|
110
|
+
selectWorkflow: workflowSelector,
|
|
111
|
+
...(defaultWorkflowId ? { defaultWorkflowId } : {}),
|
|
112
|
+
...(applyResumeSelection ? { applyResumeSelection } : {}),
|
|
113
|
+
});
|
|
114
|
+
if (resumeStream) return createStreamResponse(resumeStream);
|
|
115
|
+
|
|
116
|
+
// Determine which workflow to run (defaults to frontdesk)
|
|
117
|
+
const currentWorkflow = baseState.currentWorkflow;
|
|
118
|
+
const selectedWorkflow = await workflowSelector(currentWorkflow as string);
|
|
119
|
+
|
|
120
|
+
const graph = await createLangGraph(selectedWorkflow, runtimeConfig as any);
|
|
121
|
+
|
|
122
|
+
const orchestratedStream = await orchestrateGraphStream({
|
|
123
|
+
sessionId: resolvedSessionId,
|
|
124
|
+
graph,
|
|
125
|
+
state: { ...baseState, currentWorkflow },
|
|
126
|
+
config: runtimeConfig,
|
|
127
|
+
saveMemory,
|
|
128
|
+
selectWorkflow: workflowSelector,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return createStreamResponse(orchestratedStream as AsyncIterable<StreamChunk>);
|
|
132
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type { CreateAgentArgs } from "./chat/create-agent";
|
|
2
|
+
export { createAgent } from "./chat/create-agent";
|
|
3
|
+
export type { ProcessChatArgs } from "./chat/process-chat";
|
|
4
|
+
export { processChat } from "./chat/process-chat";
|
|
5
|
+
export type {
|
|
6
|
+
ApplyResumeSelection,
|
|
7
|
+
ResumeMeta,
|
|
8
|
+
} from "./interrupt/resume-handler";
|
|
9
|
+
export {
|
|
10
|
+
parseResumeMeta,
|
|
11
|
+
tryPrepareResumeStream,
|
|
12
|
+
} from "./interrupt/resume-handler";
|
|
13
|
+
export type {
|
|
14
|
+
CompiledGraphLike,
|
|
15
|
+
OrchestrateArgs,
|
|
16
|
+
SaveMemoryFn,
|
|
17
|
+
SelectWorkflowFn,
|
|
18
|
+
} from "./orchestrator";
|
|
19
|
+
export { orchestrateGraphStream } from "./orchestrator";
|
|
20
|
+
export { transformGraphStreamForUI } from "./stream/transform-graph-stream-for-ui";
|
|
21
|
+
export type { ChatMessage } from "./types/chat-message";
|
|
22
|
+
export { extractLatestUserMessage } from "./utils/extract-latest-message";
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { GraphState } from "@kortyx/core";
|
|
2
|
+
import {
|
|
3
|
+
deletePendingRequest,
|
|
4
|
+
getPendingRequest,
|
|
5
|
+
type PendingRequestRecord,
|
|
6
|
+
} from "@kortyx/memory";
|
|
7
|
+
import { createLangGraph } from "@kortyx/runtime";
|
|
8
|
+
import type { StreamChunk } from "@kortyx/stream";
|
|
9
|
+
import type { SaveMemoryFn, SelectWorkflowFn } from "../orchestrator";
|
|
10
|
+
import { orchestrateGraphStream } from "../orchestrator";
|
|
11
|
+
import type { ChatMessage } from "../types/chat-message";
|
|
12
|
+
|
|
13
|
+
export interface ResumeMeta {
|
|
14
|
+
token: string;
|
|
15
|
+
requestId: string;
|
|
16
|
+
selected: string[]; // normalized to array for consistency
|
|
17
|
+
cancel?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ApplyResumeSelection = (args: {
|
|
21
|
+
pending: PendingRequestRecord;
|
|
22
|
+
selected: string[];
|
|
23
|
+
}) => Record<string, unknown> | null | undefined;
|
|
24
|
+
|
|
25
|
+
export function parseResumeMeta(
|
|
26
|
+
msg: ChatMessage | undefined,
|
|
27
|
+
): ResumeMeta | null {
|
|
28
|
+
if (!msg || !msg.metadata) return null;
|
|
29
|
+
const raw = (msg.metadata as any)?.resume;
|
|
30
|
+
if (!raw) return null;
|
|
31
|
+
const token = typeof raw.token === "string" ? raw.token : "";
|
|
32
|
+
const requestId = typeof raw.requestId === "string" ? raw.requestId : "";
|
|
33
|
+
const cancel = Boolean(raw.cancel);
|
|
34
|
+
|
|
35
|
+
// Accept multiple shapes; normalize to selected: string[]
|
|
36
|
+
let selected: string[] = [];
|
|
37
|
+
if (typeof raw.selected === "string") selected = [raw.selected];
|
|
38
|
+
else if (Array.isArray(raw.selected))
|
|
39
|
+
selected = raw.selected.map((x: any) => String(x));
|
|
40
|
+
else if (raw?.choice?.id) selected = [String(raw.choice.id)];
|
|
41
|
+
else if (Array.isArray(raw?.choices))
|
|
42
|
+
selected = raw.choices.map((c: any) => String(c.id));
|
|
43
|
+
|
|
44
|
+
if (!token || !requestId) return null;
|
|
45
|
+
return { token, requestId, selected, cancel };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface TryResumeArgs {
|
|
49
|
+
lastMessage: ChatMessage | undefined;
|
|
50
|
+
sessionId: string;
|
|
51
|
+
config: Record<string, unknown>;
|
|
52
|
+
saveMemory?: SaveMemoryFn;
|
|
53
|
+
selectWorkflow: SelectWorkflowFn;
|
|
54
|
+
defaultWorkflowId?: string;
|
|
55
|
+
applyResumeSelection?: ApplyResumeSelection;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function tryPrepareResumeStream({
|
|
59
|
+
lastMessage,
|
|
60
|
+
sessionId,
|
|
61
|
+
config,
|
|
62
|
+
saveMemory,
|
|
63
|
+
selectWorkflow,
|
|
64
|
+
defaultWorkflowId,
|
|
65
|
+
applyResumeSelection,
|
|
66
|
+
}: TryResumeArgs): Promise<AsyncIterable<StreamChunk> | null> {
|
|
67
|
+
const meta = parseResumeMeta(lastMessage);
|
|
68
|
+
if (!meta) return null;
|
|
69
|
+
|
|
70
|
+
const pending = getPendingRequest(meta.token);
|
|
71
|
+
if (!pending || pending.requestId !== meta.requestId) {
|
|
72
|
+
// Invalid/expired; ignore and continue normal flow
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.log(
|
|
75
|
+
`[resume] pending not found or mismatched. token=${meta.token} requestId=${meta.requestId}`,
|
|
76
|
+
);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (meta.cancel) {
|
|
81
|
+
deletePendingRequest(pending.token);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Build a minimal state; the checkpointer (keyed by sessionId) will restore paused context
|
|
86
|
+
// eslint-disable-next-line no-console
|
|
87
|
+
console.log(
|
|
88
|
+
`[resume] token=${meta.token} requestId=${meta.requestId} selected=${JSON.stringify(
|
|
89
|
+
meta.selected,
|
|
90
|
+
)} sessionId=${sessionId}`,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const resumeData = applyResumeSelection
|
|
94
|
+
? applyResumeSelection({ pending, selected: meta.selected })
|
|
95
|
+
: meta.selected?.length
|
|
96
|
+
? { coordinates: String(meta.selected[0]) }
|
|
97
|
+
: {};
|
|
98
|
+
|
|
99
|
+
const resumeDataPatch =
|
|
100
|
+
resumeData && typeof resumeData === "object" ? resumeData : {};
|
|
101
|
+
|
|
102
|
+
const resumedState: GraphState = {
|
|
103
|
+
// For static breakpoints, resume with null input (set in orchestrator),
|
|
104
|
+
// and stash the user selection into data so the next node can read it.
|
|
105
|
+
input: "" as any,
|
|
106
|
+
lastNode: "__start__" as any,
|
|
107
|
+
currentWorkflow:
|
|
108
|
+
(pending.workflow as any) ||
|
|
109
|
+
(defaultWorkflowId as any) ||
|
|
110
|
+
("job-search" as any),
|
|
111
|
+
config: config as any,
|
|
112
|
+
conversationHistory: [],
|
|
113
|
+
awaitingHumanInput: false,
|
|
114
|
+
data: {
|
|
115
|
+
...(pending.state?.data ?? {}),
|
|
116
|
+
...resumeDataPatch,
|
|
117
|
+
} as any,
|
|
118
|
+
} as any;
|
|
119
|
+
|
|
120
|
+
const wf = await selectWorkflow(resumedState.currentWorkflow as string);
|
|
121
|
+
const resumeValue = meta.selected?.length
|
|
122
|
+
? String(meta.selected[0])
|
|
123
|
+
: undefined;
|
|
124
|
+
const resumedGraph = await createLangGraph(wf, {
|
|
125
|
+
...(config as any),
|
|
126
|
+
resume: true,
|
|
127
|
+
...(resumeValue !== undefined ? { resumeValue } : {}),
|
|
128
|
+
});
|
|
129
|
+
deletePendingRequest(pending.token);
|
|
130
|
+
|
|
131
|
+
const args: any = {
|
|
132
|
+
sessionId,
|
|
133
|
+
graph: resumedGraph,
|
|
134
|
+
state: resumedState,
|
|
135
|
+
config: {
|
|
136
|
+
...(config as any),
|
|
137
|
+
resume: true,
|
|
138
|
+
...(resumeValue !== undefined ? { resumeValue } : {}),
|
|
139
|
+
},
|
|
140
|
+
selectWorkflow,
|
|
141
|
+
};
|
|
142
|
+
if (saveMemory) args.saveMemory = saveMemory;
|
|
143
|
+
|
|
144
|
+
const stream = await orchestrateGraphStream(args);
|
|
145
|
+
return stream as AsyncIterable<StreamChunk>;
|
|
146
|
+
}
|