@h-ai/ai 1.0.0-beta.1 → 1.0.0-beta.10
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.en.md +35 -6
- package/README.ja.md +35 -6
- package/README.md +35 -6
- package/dist/{ai-audio-ws-protocol-nfhO0tOo.d.ts → ai-acp-types-DQ8jO9Ie.d.ts} +2387 -229
- package/dist/browser.d.ts +4 -3
- package/dist/browser.js +3 -3
- package/dist/{chunk-HEVXR252.js → chunk-HEYVFQC6.js} +290 -23
- package/dist/chunk-HEYVFQC6.js.map +1 -0
- package/dist/{chunk-HYMP57AC.js → chunk-M77C3VAP.js} +23 -2
- package/dist/chunk-M77C3VAP.js.map +1 -0
- package/dist/{chunk-GMQFIWND.js → chunk-O6P4P5PC.js} +3 -3
- package/dist/{chunk-GMQFIWND.js.map → chunk-O6P4P5PC.js.map} +1 -1
- package/dist/client/index.js +2 -2
- package/dist/index.d.ts +5 -11
- package/dist/index.js +3326 -158
- package/dist/index.js.map +1 -1
- package/models/agents/README.md +48 -0
- package/models/agents/build.mjs +66 -0
- package/models/agents/codex-acp/Dockerfile +25 -0
- package/models/agents/codex-acp/agent.json +16 -0
- package/models/agents/deepseek-harness/Dockerfile +23 -0
- package/models/agents/deepseek-harness/agent.json +14 -0
- package/models/agents/goose/Dockerfile +34 -0
- package/models/agents/goose/agent.json +14 -0
- package/package.json +12 -8
- package/dist/chunk-HEVXR252.js.map +0 -1
- package/dist/chunk-HYMP57AC.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { aiM, AIConfigSchema, MemoryConfigSchema,
|
|
2
|
-
export { A2AConfigSchema, A2ASkillConfigSchema, AIConfigSchema, AUDIO_WS_PATH, ApiTypeSchema, AudioConfigSchema, AudioFormatSchema, AudioModelEntrySchema, AudioOperationSchema, AudioOperationsSchema, AudioProviderSchema, AudioWsClientMessageSchema, AudioWsDoneMessageSchema, AudioWsStartMessageSchema, AudioWsTextMessageSchema, CompressConfigSchema, CompressionStrategySchema, EmbeddingConfigSchema, EntityTypeSchema, FileConfigSchema, ImageConfigSchema, ImageModelEntrySchema, ImageProviderSchema, KnowledgeConfigSchema, LLMConfigSchema, MCPConfigSchema, MCPServerCapabilitiesSchema, MCPServerConfigSchema, MemoryConfigSchema, MemoryTypeSchema, ModelEntrySchema, ModelScenarioSchema, OptionalSecretSchema, RetrievalConfigSchema, RetrievalSourceSchema, SummaryConfigSchema, TokenConfigSchema, ensureAudioCredential, resolveAudioModel, resolveImageModel, resolveModelApi, resolveModelEntry, serializePlayableAudio } from './chunk-
|
|
3
|
-
import { HaiAIError } from './chunk-
|
|
4
|
-
export { HaiAIError } from './chunk-
|
|
5
|
-
import
|
|
1
|
+
import { aiM, AIConfigSchema, MemoryConfigSchema, ModelEntrySchema, AudioModelEntrySchema, ImageModelEntrySchema, MCPExternalServerSchema, RetrievalSourceSchema, KnowledgeConfigSchema, TokenConfigSchema, SummaryConfigSchema, CompressConfigSchema, ACPConfigSchema, ACPAgentSchema, ACPWorkspaceSchema, AudioConfigSchema, resolveModelEntry, resolveAudioModel, ensureAudioCredential, resolveModelApi, sessionIdentitySchema, ImageConfigSchema, resolveImageModel, ACPPublishSchema, branchNameSchema } from './chunk-HEYVFQC6.js';
|
|
2
|
+
export { A2AConfigSchema, A2ASkillConfigSchema, ACPAgentSchema, ACPConfigSchema, ACPPublishSchema, ACPRepositorySchema, ACPWorkspaceSchema, AIConfigSchema, AUDIO_WS_PATH, ApiTypeSchema, AudioConfigSchema, AudioFormatSchema, AudioModelEntrySchema, AudioOperationSchema, AudioOperationsSchema, AudioProviderSchema, AudioWsClientMessageSchema, AudioWsDoneMessageSchema, AudioWsStartMessageSchema, AudioWsTextMessageSchema, CompressConfigSchema, CompressionStrategySchema, EmbeddingConfigSchema, EntityTypeSchema, FileConfigSchema, ImageConfigSchema, ImageModelEntrySchema, ImageProviderSchema, KnowledgeConfigSchema, LLMConfigSchema, MCPConfigSchema, MCPExternalServerSchema, MCPServerCapabilitiesSchema, MCPServerConfigSchema, MemoryConfigSchema, MemoryTypeSchema, ModelCapabilitiesSchema, ModelEntrySchema, ModelScenarioSchema, OptionalSecretSchema, RetrievalConfigSchema, RetrievalSourceSchema, SummaryConfigSchema, TokenConfigSchema, ensureAudioCredential, resolveAudioModel, resolveImageModel, resolveModelApi, resolveModelEntry, serializePlayableAudio } from './chunk-HEYVFQC6.js';
|
|
3
|
+
import { HaiAIError } from './chunk-M77C3VAP.js';
|
|
4
|
+
export { HaiAIError } from './chunk-M77C3VAP.js';
|
|
5
|
+
import process2 from 'process';
|
|
6
6
|
import { core, err, ok } from '@h-ai/core';
|
|
7
7
|
import { datapipe } from '@h-ai/datapipe';
|
|
8
8
|
import { A2AClient } from '@a2a-js/sdk/client';
|
|
@@ -10,14 +10,21 @@ import { JsonRpcTransportHandler, ServerCallContext, A2AError, RequestContext, R
|
|
|
10
10
|
import { Buffer } from 'buffer';
|
|
11
11
|
import { createHash, randomUUID } from 'crypto';
|
|
12
12
|
import { setTimeout as setTimeout$1 } from 'timers/promises';
|
|
13
|
+
import { z, ZodError } from 'zod';
|
|
14
|
+
import { mkdir, realpath } from 'fs/promises';
|
|
15
|
+
import { isAbsolute, resolve, relative, dirname, basename } from 'path';
|
|
16
|
+
import { spawn } from 'child_process';
|
|
17
|
+
import { Writable, Readable } from 'stream';
|
|
18
|
+
import { methods, ndJsonStream, client, PROTOCOL_VERSION } from '@agentclientprotocol/sdk';
|
|
19
|
+
import { createHttpStream } from '@agentclientprotocol/sdk/experimental/http-client';
|
|
20
|
+
import { createWebSocketStream } from '@agentclientprotocol/sdk/experimental/ws-client';
|
|
21
|
+
import WebSocket2, { WebSocket } from 'ws';
|
|
13
22
|
import { gunzipSync } from 'zlib';
|
|
14
|
-
import WebSocket from 'ws';
|
|
15
23
|
import { FormData as FormData$1, Agent, fetch as fetch$1 } from 'undici';
|
|
16
24
|
import OpenAI3, { toFile } from 'openai';
|
|
17
25
|
import { nanoid } from 'nanoid';
|
|
18
26
|
import dns from 'dns';
|
|
19
27
|
import { BlockList, isIP } from 'net';
|
|
20
|
-
import { z } from 'zod';
|
|
21
28
|
import { PromptMessageSchema, TextResourceContentsSchema, BlobResourceContentsSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
22
29
|
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv';
|
|
23
30
|
import { reldb } from '@h-ai/reldb';
|
|
@@ -520,7 +527,7 @@ async function executeRemoteAgentCall(remoteUrl, message, options) {
|
|
|
520
527
|
safeLabel = validatedUrl.safeLabel;
|
|
521
528
|
const customHeaders = options?.headers;
|
|
522
529
|
const timeoutMs = Math.max(1, options?.timeout ?? DEFAULT_A2A_CALL_TIMEOUT_MS);
|
|
523
|
-
const
|
|
530
|
+
const client2 = new A2AClient(requestUrl, {
|
|
524
531
|
fetchImpl: ((input, init) => {
|
|
525
532
|
const existingHeaders = init?.headers ? Object.fromEntries(new Headers(init.headers).entries()) : {};
|
|
526
533
|
const signals = [init?.signal, AbortSignal.timeout(timeoutMs)].filter((signal) => Boolean(signal));
|
|
@@ -540,7 +547,7 @@ async function executeRemoteAgentCall(remoteUrl, message, options) {
|
|
|
540
547
|
parts: [{ kind: "text", text: message }]
|
|
541
548
|
}
|
|
542
549
|
};
|
|
543
|
-
const response = await
|
|
550
|
+
const response = await client2.sendMessage(params);
|
|
544
551
|
if ("error" in response) {
|
|
545
552
|
return err(HaiAIError.A2A_REMOTE_CALL_FAILED, aiM("ai_a2aRemoteCallFailed", { params: { url: safeLabel, error: "Remote agent returned an error" } }));
|
|
546
553
|
}
|
|
@@ -702,6 +709,2766 @@ function createA2ALazyProxy(deps) {
|
|
|
702
709
|
}
|
|
703
710
|
};
|
|
704
711
|
}
|
|
712
|
+
var ACPFailure = class extends Error {
|
|
713
|
+
/**
|
|
714
|
+
* 创建只携带允许公开诊断信息的内部异常。
|
|
715
|
+
* @param kind - 固定错误类别。
|
|
716
|
+
* @param details - 脱敏数据,不得包含凭据或原始 stderr。
|
|
717
|
+
* @returns 内部异常实例。
|
|
718
|
+
* @example
|
|
719
|
+
* ```ts
|
|
720
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
721
|
+
* const failure = new ACPFailure('busy')
|
|
722
|
+
* ```
|
|
723
|
+
*/
|
|
724
|
+
constructor(kind, details) {
|
|
725
|
+
super(kind);
|
|
726
|
+
this.kind = kind;
|
|
727
|
+
this.details = details;
|
|
728
|
+
}
|
|
729
|
+
kind;
|
|
730
|
+
details;
|
|
731
|
+
};
|
|
732
|
+
var mappings = {
|
|
733
|
+
invalid: ["ACP_INVALID_REQUEST", "ai_acpInvalid"],
|
|
734
|
+
missing: ["ACP_NOT_FOUND", "ai_acpNotFound"],
|
|
735
|
+
busy: ["ACP_BUSY", "ai_acpBusy"],
|
|
736
|
+
unsupported: ["ACP_UNSUPPORTED", "ai_acpUnsupported"],
|
|
737
|
+
cancelled: ["ACP_CANCELLED", "ai_acpCancelled"],
|
|
738
|
+
timeout: ["ACP_TIMEOUT", "ai_acpTimeout"],
|
|
739
|
+
output: ["ACP_OUTPUT_INVALID", "ai_acpOutput"],
|
|
740
|
+
conflict: ["ACP_CONFLICT", "ai_acpConflict"],
|
|
741
|
+
partial: ["ACP_PARTIAL_FAILURE", "ai_acpPartial"],
|
|
742
|
+
closed: ["ACP_CLOSED", "ai_acpClosed"],
|
|
743
|
+
runtime: ["ACP_RUNTIME_UNAVAILABLE", "ai_acpRuntime"],
|
|
744
|
+
model: ["ACP_MODEL_INCOMPATIBLE", "ai_acpModel"],
|
|
745
|
+
cleanup: ["ACP_CLEANUP_FAILED", "ai_acpCleanup"],
|
|
746
|
+
gitMissing: ["ACP_GIT_NOT_FOUND", "ai_acpGitNotFound"],
|
|
747
|
+
failed: ["ACP_OPERATION_FAILED", "ai_acpOperationFailed"]
|
|
748
|
+
};
|
|
749
|
+
function acpError(error) {
|
|
750
|
+
const failure = error instanceof ACPFailure ? error : new ACPFailure(error instanceof ZodError ? "invalid" : "failed");
|
|
751
|
+
const [code, message] = mappings[failure.kind];
|
|
752
|
+
return err(HaiAIError[code], aiM(message), failure.details);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/acp/ai-acp-adapters.ts
|
|
756
|
+
function resolveAgentModels(agent, config) {
|
|
757
|
+
return agent.modelIds.map((id) => {
|
|
758
|
+
if (!config.llm?.models?.some((entry2) => entry2.id === id))
|
|
759
|
+
throw new ACPFailure("model", { modelId: id });
|
|
760
|
+
const entry = config.llm.models.find((model) => model.id === id);
|
|
761
|
+
const protocol = entry.protocol ?? config.llm.protocol;
|
|
762
|
+
const llm = protocol === "anthropic" ? {
|
|
763
|
+
...config.llm,
|
|
764
|
+
apiKey: entry.apiKey ?? config.llm.apiKey ?? (process2.env.HAI_AI_LLM_APIKEY?.trim() || process2.env.ANTHROPIC_API_KEY),
|
|
765
|
+
baseUrl: entry.baseUrl ?? (config.llm.protocol === "anthropic" ? config.llm.baseUrl ?? process2.env.HAI_AI_LLM_BASEURL : void 0) ?? process2.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com"
|
|
766
|
+
} : config.llm;
|
|
767
|
+
if (protocol === "anthropic" && !llm.apiKey)
|
|
768
|
+
throw new ACPFailure("model", { modelId: id });
|
|
769
|
+
const result = resolveModelEntry(llm, "default", id);
|
|
770
|
+
if (!result.success)
|
|
771
|
+
throw new ACPFailure("model", { modelId: id });
|
|
772
|
+
if (result.data.capabilities?.toolCalling === false)
|
|
773
|
+
throw new ACPFailure("model", { modelId: id });
|
|
774
|
+
return { id, config: result.data };
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
function resolveEnvironment(references) {
|
|
778
|
+
const env = {};
|
|
779
|
+
for (const [key, source] of Object.entries(references)) {
|
|
780
|
+
const value = process2.env[source];
|
|
781
|
+
if (!value)
|
|
782
|
+
throw new ACPFailure("invalid", { environment: source });
|
|
783
|
+
env[key] = value;
|
|
784
|
+
}
|
|
785
|
+
return env;
|
|
786
|
+
}
|
|
787
|
+
var nativeAdapters = {
|
|
788
|
+
"deepseek-harness": {
|
|
789
|
+
configure({ agent, models, modelId, statePath }) {
|
|
790
|
+
const selected = models.findIndex((item) => item.id === modelId);
|
|
791
|
+
if (selected < 0)
|
|
792
|
+
throw new ACPFailure("model", { modelId });
|
|
793
|
+
const env = { DSH_HOME: statePath };
|
|
794
|
+
const providers = {};
|
|
795
|
+
for (const [index, entry] of models.entries()) {
|
|
796
|
+
const model = entry.config;
|
|
797
|
+
const caps = model.capabilities;
|
|
798
|
+
if (!caps?.contextWindow || !caps.maxOutputTokens || !caps.inputModalities || caps.toolCalling !== true || !caps.reasoning || caps.reasoning.supported && !caps.reasoning.efforts?.length) {
|
|
799
|
+
throw new ACPFailure("model", { modelId: entry.id });
|
|
800
|
+
}
|
|
801
|
+
if (caps.inputModalities.some((item) => !["text", "image"].includes(item)))
|
|
802
|
+
throw new ACPFailure("model", { modelId: entry.id });
|
|
803
|
+
const key = `HAI_ACP_MODEL_${index}`;
|
|
804
|
+
env[key] = model.apiKey;
|
|
805
|
+
providers[`hai-${index}`] = {
|
|
806
|
+
apiKeyEnv: key,
|
|
807
|
+
baseURL: model.baseUrl,
|
|
808
|
+
api: model.protocol === "chat" ? "openai-completions" : model.protocol === "responses" ? "openai-responses" : "anthropic-messages",
|
|
809
|
+
// 输出容量是能力声明;单轮预算沿用 Hai 模型的 maxTokens。
|
|
810
|
+
defaultMaxTokens: Math.min(model.maxTokens, caps.maxOutputTokens),
|
|
811
|
+
defaultContextWindow: caps.contextWindow,
|
|
812
|
+
timeoutMs: model.timeout,
|
|
813
|
+
retryPolicy: { mode: "normal", maxRetries: 0 },
|
|
814
|
+
models: [
|
|
815
|
+
{
|
|
816
|
+
id: model.model,
|
|
817
|
+
contextWindow: caps.contextWindow,
|
|
818
|
+
input: caps.inputModalities,
|
|
819
|
+
reasoningEfforts: caps.reasoning.supported ? Object.fromEntries(
|
|
820
|
+
(caps.reasoning.efforts ?? []).map((effort) => [
|
|
821
|
+
effort,
|
|
822
|
+
effort
|
|
823
|
+
])
|
|
824
|
+
) : false
|
|
825
|
+
}
|
|
826
|
+
]
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
const patch = [
|
|
830
|
+
{ id: "llm-pi-ai", config: { providers } },
|
|
831
|
+
{
|
|
832
|
+
id: "acp",
|
|
833
|
+
config: {
|
|
834
|
+
provider: `hai-${selected}`,
|
|
835
|
+
model: models[selected].config.model
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
];
|
|
839
|
+
return {
|
|
840
|
+
command: agent.mode === "custom-service" ? "dsh" : agent.command ?? "dsh",
|
|
841
|
+
args: [
|
|
842
|
+
// 自定义命令可能是 node + JS 入口,入口参数必须先于 dsh 的原生选项。
|
|
843
|
+
...agent.mode === "custom-service" ? [] : agent.args ?? [],
|
|
844
|
+
"--profile",
|
|
845
|
+
"acp",
|
|
846
|
+
"--patch",
|
|
847
|
+
`${statePath}/hai-config.json`
|
|
848
|
+
],
|
|
849
|
+
env,
|
|
850
|
+
files: { "hai-config.json": JSON.stringify(patch) }
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
"generic": {
|
|
855
|
+
configure({ agent, models }) {
|
|
856
|
+
if (agent.mode === "custom-service" || !agent.command || models.length)
|
|
857
|
+
throw new ACPFailure("model");
|
|
858
|
+
return { command: agent.command, args: agent.args ?? [], env: {} };
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
"codex": {
|
|
862
|
+
configure({ agent, models, modelId, statePath }) {
|
|
863
|
+
const selected = models.find((item) => item.id === modelId);
|
|
864
|
+
if (!selected || selected.config.protocol !== "responses")
|
|
865
|
+
throw new ACPFailure("model", { modelId });
|
|
866
|
+
const model = selected.config;
|
|
867
|
+
return {
|
|
868
|
+
command: agent.mode === "custom-service" ? "codex-acp" : agent.command ?? "codex-acp",
|
|
869
|
+
args: agent.mode === "custom-service" ? [] : agent.args ?? [],
|
|
870
|
+
env: {
|
|
871
|
+
CODEX_HOME: statePath,
|
|
872
|
+
CODEX_API_KEY: model.apiKey,
|
|
873
|
+
OPENAI_API_KEY: model.apiKey,
|
|
874
|
+
NO_BROWSER: "1",
|
|
875
|
+
DEFAULT_AUTH_REQUEST: JSON.stringify({ methodId: "api-key" }),
|
|
876
|
+
MODEL_PROVIDER: "hai",
|
|
877
|
+
CODEX_CONFIG: JSON.stringify({
|
|
878
|
+
model: model.model,
|
|
879
|
+
model_provider: "hai",
|
|
880
|
+
model_context_window: model.capabilities?.contextWindow,
|
|
881
|
+
model_providers: {
|
|
882
|
+
hai: {
|
|
883
|
+
name: "Hai",
|
|
884
|
+
base_url: model.baseUrl,
|
|
885
|
+
wire_api: "responses",
|
|
886
|
+
env_key: "CODEX_API_KEY"
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
})
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
function createResourceRegistry(store, schema, configured, isOpen) {
|
|
896
|
+
const config = new Map(
|
|
897
|
+
configured.map((item) => [item.id, structuredClone(item)])
|
|
898
|
+
);
|
|
899
|
+
if (config.size !== configured.length)
|
|
900
|
+
throw new Error("Duplicate configuration resource ID");
|
|
901
|
+
const copy = (record) => {
|
|
902
|
+
const { __deleted: _deleted, ...value } = record;
|
|
903
|
+
return structuredClone(value);
|
|
904
|
+
};
|
|
905
|
+
function ensure() {
|
|
906
|
+
if (!isOpen())
|
|
907
|
+
throw new Error("Resource registry closed");
|
|
908
|
+
}
|
|
909
|
+
async function read(id) {
|
|
910
|
+
ensure();
|
|
911
|
+
const fixed = config.get(id);
|
|
912
|
+
if (fixed)
|
|
913
|
+
return { value: structuredClone(fixed), source: "config" };
|
|
914
|
+
const saved = await store.readVersioned(id);
|
|
915
|
+
return saved && !saved.data.__deleted ? { value: copy(saved.data), source: "database", version: saved.version } : void 0;
|
|
916
|
+
}
|
|
917
|
+
async function entries() {
|
|
918
|
+
ensure();
|
|
919
|
+
const saved = await store.query({});
|
|
920
|
+
return [
|
|
921
|
+
...[...config.values()].map((value) => ({
|
|
922
|
+
value: structuredClone(value),
|
|
923
|
+
source: "config"
|
|
924
|
+
})),
|
|
925
|
+
...saved.filter((item) => !item.__deleted && !config.has(item.id)).map((item) => ({ value: copy(item), source: "database" }))
|
|
926
|
+
];
|
|
927
|
+
}
|
|
928
|
+
async function protect(operation) {
|
|
929
|
+
if (!isOpen())
|
|
930
|
+
return err(HaiAIError.NOT_INITIALIZED, aiM("ai_notInitialized"));
|
|
931
|
+
try {
|
|
932
|
+
return ok(await operation());
|
|
933
|
+
} catch {
|
|
934
|
+
return err(
|
|
935
|
+
HaiAIError.RESOURCE_OPERATION_FAILED,
|
|
936
|
+
aiM("ai_resourceOperationFailed")
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async function write(input, update, expectedVersion) {
|
|
941
|
+
if (!isOpen())
|
|
942
|
+
return err(HaiAIError.NOT_INITIALIZED, aiM("ai_notInitialized"));
|
|
943
|
+
const parsed = schema.safeParse(input);
|
|
944
|
+
if (!parsed.success || !parsed.data.id)
|
|
945
|
+
return err(HaiAIError.RESOURCE_INVALID, aiM("ai_resourceInvalid"));
|
|
946
|
+
const value = parsed.data;
|
|
947
|
+
if (config.has(value.id))
|
|
948
|
+
return err(HaiAIError.RESOURCE_READ_ONLY, aiM("ai_resourceReadOnly"));
|
|
949
|
+
try {
|
|
950
|
+
const previous = await store.readVersioned(value.id);
|
|
951
|
+
const exists = previous && !previous.data.__deleted;
|
|
952
|
+
if (update && !exists)
|
|
953
|
+
return err(HaiAIError.RESOURCE_NOT_FOUND, aiM("ai_resourceNotFound"));
|
|
954
|
+
if (!update && exists || expectedVersion !== void 0 && expectedVersion !== previous?.version) {
|
|
955
|
+
return err(HaiAIError.RESOURCE_CONFLICT, aiM("ai_resourceConflict"));
|
|
956
|
+
}
|
|
957
|
+
const version = await store.compareAndSwap(value.id, {
|
|
958
|
+
expectedVersion: previous?.version,
|
|
959
|
+
data: value
|
|
960
|
+
});
|
|
961
|
+
if (!version)
|
|
962
|
+
return err(HaiAIError.RESOURCE_CONFLICT, aiM("ai_resourceConflict"));
|
|
963
|
+
return ok({
|
|
964
|
+
value: structuredClone(value),
|
|
965
|
+
source: "database",
|
|
966
|
+
version
|
|
967
|
+
});
|
|
968
|
+
} catch {
|
|
969
|
+
return err(
|
|
970
|
+
HaiAIError.RESOURCE_OPERATION_FAILED,
|
|
971
|
+
aiM("ai_resourceOperationFailed")
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
const operations = {
|
|
976
|
+
list: () => protect(entries),
|
|
977
|
+
get: async (id) => {
|
|
978
|
+
const result = await protect(() => read(id));
|
|
979
|
+
if (!result.success)
|
|
980
|
+
return result;
|
|
981
|
+
return result.data ? ok(result.data) : err(HaiAIError.RESOURCE_NOT_FOUND, aiM("ai_resourceNotFound"));
|
|
982
|
+
},
|
|
983
|
+
register: (input) => write(input, false),
|
|
984
|
+
update: (input, version) => write(input, true, version),
|
|
985
|
+
remove: async (id, expectedVersion) => {
|
|
986
|
+
if (!isOpen())
|
|
987
|
+
return err(HaiAIError.NOT_INITIALIZED, aiM("ai_notInitialized"));
|
|
988
|
+
if (config.has(id))
|
|
989
|
+
return err(HaiAIError.RESOURCE_READ_ONLY, aiM("ai_resourceReadOnly"));
|
|
990
|
+
try {
|
|
991
|
+
const previous = await store.readVersioned(id);
|
|
992
|
+
if (!previous || previous.data.__deleted)
|
|
993
|
+
return err(HaiAIError.RESOURCE_NOT_FOUND, aiM("ai_resourceNotFound"));
|
|
994
|
+
if (expectedVersion !== void 0 && expectedVersion !== previous.version) {
|
|
995
|
+
return err(HaiAIError.RESOURCE_CONFLICT, aiM("ai_resourceConflict"));
|
|
996
|
+
}
|
|
997
|
+
if (!await store.compareAndSwap(id, {
|
|
998
|
+
expectedVersion: previous.version,
|
|
999
|
+
data: { ...previous.data, __deleted: true }
|
|
1000
|
+
})) {
|
|
1001
|
+
return err(HaiAIError.RESOURCE_CONFLICT, aiM("ai_resourceConflict"));
|
|
1002
|
+
}
|
|
1003
|
+
return ok(void 0);
|
|
1004
|
+
} catch {
|
|
1005
|
+
return err(
|
|
1006
|
+
HaiAIError.RESOURCE_OPERATION_FAILED,
|
|
1007
|
+
aiM("ai_resourceOperationFailed")
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
return {
|
|
1013
|
+
operations,
|
|
1014
|
+
read,
|
|
1015
|
+
entries,
|
|
1016
|
+
values: async () => (await entries()).map((entry) => entry.value)
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
var nativeJob = String.raw`
|
|
1020
|
+
using System;
|
|
1021
|
+
using System.ComponentModel;
|
|
1022
|
+
using System.Runtime.InteropServices;
|
|
1023
|
+
using System.Text;
|
|
1024
|
+
using System.Threading;
|
|
1025
|
+
|
|
1026
|
+
public static class HaiACPCommandJob {
|
|
1027
|
+
[StructLayout(LayoutKind.Sequential)] struct BasicLimits {
|
|
1028
|
+
public long ProcessTime, JobTime;
|
|
1029
|
+
public uint Flags;
|
|
1030
|
+
public UIntPtr MinimumWorkingSet, MaximumWorkingSet;
|
|
1031
|
+
public uint ActiveProcessLimit;
|
|
1032
|
+
public UIntPtr Affinity;
|
|
1033
|
+
public uint PriorityClass, SchedulingClass;
|
|
1034
|
+
}
|
|
1035
|
+
[StructLayout(LayoutKind.Sequential)] struct IoCounters {
|
|
1036
|
+
public ulong ReadOperations, WriteOperations, OtherOperations, ReadBytes, WriteBytes, OtherBytes;
|
|
1037
|
+
}
|
|
1038
|
+
[StructLayout(LayoutKind.Sequential)] struct ExtendedLimits {
|
|
1039
|
+
public BasicLimits Basic;
|
|
1040
|
+
public IoCounters Io;
|
|
1041
|
+
public UIntPtr ProcessMemory, JobMemory, PeakProcessMemory, PeakJobMemory;
|
|
1042
|
+
}
|
|
1043
|
+
[StructLayout(LayoutKind.Sequential)] struct Accounting {
|
|
1044
|
+
public long UserTime, KernelTime, PeriodUserTime, PeriodKernelTime;
|
|
1045
|
+
public uint Faults, Total, Active, Terminated;
|
|
1046
|
+
}
|
|
1047
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct Startup {
|
|
1048
|
+
public uint Size;
|
|
1049
|
+
public string Reserved, Desktop, Title;
|
|
1050
|
+
public uint X, Y, Width, Height, XChars, YChars, Fill, Flags;
|
|
1051
|
+
public ushort Show, ReservedSize;
|
|
1052
|
+
public IntPtr ReservedData, Input, Output, Error;
|
|
1053
|
+
}
|
|
1054
|
+
[StructLayout(LayoutKind.Sequential)] struct ProcessInfo {
|
|
1055
|
+
public IntPtr Process, Thread;
|
|
1056
|
+
public uint ProcessId, ThreadId;
|
|
1057
|
+
}
|
|
1058
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern IntPtr CreateJobObject(IntPtr attributes, string name);
|
|
1059
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern IntPtr OpenJobObject(uint access, bool inherit, string name);
|
|
1060
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool SetInformationJobObject(IntPtr job, int kind, ref ExtendedLimits information, uint size);
|
|
1061
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool QueryInformationJobObject(IntPtr job, int kind, out Accounting information, uint size, IntPtr length);
|
|
1062
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
|
|
1063
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool CreateProcess(string application, StringBuilder command, IntPtr processAttributes, IntPtr threadAttributes, bool inherit, uint flags, IntPtr environment, string directory, ref Startup startup, out ProcessInfo information);
|
|
1064
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern uint ResumeThread(IntPtr thread);
|
|
1065
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateProcess(IntPtr process, uint code);
|
|
1066
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateJobObject(IntPtr job, uint code);
|
|
1067
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
|
|
1068
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool GetExitCodeProcess(IntPtr process, out uint code);
|
|
1069
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr handle);
|
|
1070
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr GetStdHandle(int kind);
|
|
1071
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags);
|
|
1072
|
+
|
|
1073
|
+
static void Check(bool success) { if (!success) throw new Win32Exception(Marshal.GetLastWin32Error()); }
|
|
1074
|
+
static uint Active(IntPtr job) {
|
|
1075
|
+
Accounting information;
|
|
1076
|
+
Check(QueryInformationJobObject(job, 1, out information, (uint)Marshal.SizeOf(typeof(Accounting)), IntPtr.Zero));
|
|
1077
|
+
return information.Active;
|
|
1078
|
+
}
|
|
1079
|
+
// Windows argv 编码:引号前与结尾的反斜杠加倍,其余字符原样传递,不经过命令 shell。
|
|
1080
|
+
static string Quote(string value) {
|
|
1081
|
+
StringBuilder result = new StringBuilder("\"");
|
|
1082
|
+
int slashes = 0;
|
|
1083
|
+
foreach (char current in value) {
|
|
1084
|
+
if (current == '\\') { slashes++; continue; }
|
|
1085
|
+
if (current == '"') result.Append('\\', slashes * 2 + 1);
|
|
1086
|
+
else result.Append('\\', slashes);
|
|
1087
|
+
result.Append(current);
|
|
1088
|
+
slashes = 0;
|
|
1089
|
+
}
|
|
1090
|
+
result.Append('\\', slashes * 2).Append('"');
|
|
1091
|
+
return result.ToString();
|
|
1092
|
+
}
|
|
1093
|
+
public static int Run(string name, string command, string[] arguments) {
|
|
1094
|
+
IntPtr job = CreateJobObject(IntPtr.Zero, name);
|
|
1095
|
+
if (job == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
1096
|
+
ProcessInfo process = new ProcessInfo();
|
|
1097
|
+
bool assigned = false;
|
|
1098
|
+
try {
|
|
1099
|
+
ExtendedLimits limits = new ExtendedLimits();
|
|
1100
|
+
limits.Basic.Flags = 0x2000; // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;不允许 breakaway。
|
|
1101
|
+
Check(SetInformationJobObject(job, 9, ref limits, (uint)Marshal.SizeOf(typeof(ExtendedLimits))));
|
|
1102
|
+
StringBuilder line = new StringBuilder(Quote(command));
|
|
1103
|
+
foreach (string argument in arguments) line.Append(' ').Append(Quote(argument));
|
|
1104
|
+
Startup startup = new Startup();
|
|
1105
|
+
startup.Size = (uint)Marshal.SizeOf(typeof(Startup));
|
|
1106
|
+
startup.Flags = 0x100;
|
|
1107
|
+
startup.Input = GetStdHandle(-10); startup.Output = GetStdHandle(-11); startup.Error = GetStdHandle(-12);
|
|
1108
|
+
Check(SetHandleInformation(startup.Input, 1, 1));
|
|
1109
|
+
Check(SetHandleInformation(startup.Output, 1, 1));
|
|
1110
|
+
Check(SetHandleInformation(startup.Error, 1, 1));
|
|
1111
|
+
Check(CreateProcess(null, line, IntPtr.Zero, IntPtr.Zero, true, 0x08000004, IntPtr.Zero, null, ref startup, out process));
|
|
1112
|
+
Check(AssignProcessToJobObject(job, process.Process));
|
|
1113
|
+
assigned = true;
|
|
1114
|
+
if (ResumeThread(process.Thread) == 0xFFFFFFFF) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
1115
|
+
if (WaitForSingleObject(process.Process, 0xFFFFFFFF) != 0) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
1116
|
+
uint code;
|
|
1117
|
+
Check(GetExitCodeProcess(process.Process, out code));
|
|
1118
|
+
while (Active(job) != 0) Thread.Sleep(20);
|
|
1119
|
+
return unchecked((int)code);
|
|
1120
|
+
}
|
|
1121
|
+
finally {
|
|
1122
|
+
if (process.Process != IntPtr.Zero) {
|
|
1123
|
+
// 分配失败时进程仍挂起,直接终止,不允许裸进程恢复;Job 在其他异常时同样清空。
|
|
1124
|
+
if (!assigned) TerminateProcess(process.Process, 125);
|
|
1125
|
+
else TerminateJobObject(job, 125);
|
|
1126
|
+
WaitForSingleObject(process.Process, 5000);
|
|
1127
|
+
}
|
|
1128
|
+
if (process.Thread != IntPtr.Zero) CloseHandle(process.Thread);
|
|
1129
|
+
if (process.Process != IntPtr.Zero) CloseHandle(process.Process);
|
|
1130
|
+
CloseHandle(job);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
public static bool Empty(string name) {
|
|
1134
|
+
IntPtr job = OpenJobObject(4, false, name); // JOB_OBJECT_QUERY
|
|
1135
|
+
if (job == IntPtr.Zero) {
|
|
1136
|
+
if (Marshal.GetLastWin32Error() == 2) return true;
|
|
1137
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
1138
|
+
}
|
|
1139
|
+
try { return Active(job) == 0; }
|
|
1140
|
+
finally { CloseHandle(job); }
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
`;
|
|
1144
|
+
var compile = `Import-Module ([IO.Path]::Combine($PSHOME, 'Modules', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Utility.psd1'))
|
|
1145
|
+
Add-Type -TypeDefinition @'
|
|
1146
|
+
${nativeJob}
|
|
1147
|
+
'@`;
|
|
1148
|
+
function powershell(script) {
|
|
1149
|
+
return ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")];
|
|
1150
|
+
}
|
|
1151
|
+
function windowsACPCommand(command, args) {
|
|
1152
|
+
const name = `Local\\hai-ai-acp-${randomUUID()}`;
|
|
1153
|
+
const key = `__HAI_AI_ACP_JOB_${randomUUID().replaceAll("-", "")}`;
|
|
1154
|
+
const script = `$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $InformationPreference='SilentlyContinue'
|
|
1155
|
+
try {
|
|
1156
|
+
${compile}
|
|
1157
|
+
$inputConfig = [Environment]::GetEnvironmentVariable('${key}') | ConvertFrom-Json
|
|
1158
|
+
[Environment]::SetEnvironmentVariable('${key}', $null)
|
|
1159
|
+
exit ([HaiACPCommandJob]::Run([string]$inputConfig.name, [string]$inputConfig.command, [string[]]$inputConfig.args))
|
|
1160
|
+
} catch { [Console]::Error.WriteLine('Windows ACP job failed'); exit 125 }`;
|
|
1161
|
+
return { name, args: powershell(script), env: { [key]: JSON.stringify({ name, command, args }) } };
|
|
1162
|
+
}
|
|
1163
|
+
function windowsACPJobProbe(name) {
|
|
1164
|
+
return powershell(`$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $InformationPreference='SilentlyContinue'
|
|
1165
|
+
try {
|
|
1166
|
+
${compile}
|
|
1167
|
+
if ([HaiACPCommandJob]::Empty('${name}')) { exit 0 } else { exit 1 }
|
|
1168
|
+
} catch {
|
|
1169
|
+
$cause = $_.Exception.GetBaseException()
|
|
1170
|
+
[Console]::Error.WriteLine(('Windows ACP job probe failed ({0}; win32={1})' -f $cause.GetType().Name, $cause.NativeErrorCode))
|
|
1171
|
+
exit 2
|
|
1172
|
+
}`);
|
|
1173
|
+
}
|
|
1174
|
+
async function jobProbe(name, timeout) {
|
|
1175
|
+
return new Promise((resolve3) => {
|
|
1176
|
+
const probe = spawn("powershell.exe", windowsACPJobProbe(name), { shell: false, windowsHide: true, stdio: "ignore", env: baseEnvironment() });
|
|
1177
|
+
let settled = false;
|
|
1178
|
+
let timer;
|
|
1179
|
+
const finish = (code) => {
|
|
1180
|
+
if (settled)
|
|
1181
|
+
return;
|
|
1182
|
+
settled = true;
|
|
1183
|
+
clearTimeout(timer);
|
|
1184
|
+
resolve3(code);
|
|
1185
|
+
};
|
|
1186
|
+
timer = setTimeout(() => {
|
|
1187
|
+
void stopProcess(probe).catch(() => {
|
|
1188
|
+
}).finally(() => finish(2));
|
|
1189
|
+
probe.kill("SIGKILL");
|
|
1190
|
+
finish(2);
|
|
1191
|
+
}, Math.max(1, timeout));
|
|
1192
|
+
probe.once("error", () => finish(2));
|
|
1193
|
+
probe.once("close", (code) => finish(code ?? 2));
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
async function stopWindowsJob(child, name, closeObserved) {
|
|
1197
|
+
const deadline = performance.now() + 15e3;
|
|
1198
|
+
let timer;
|
|
1199
|
+
try {
|
|
1200
|
+
await Promise.race([
|
|
1201
|
+
stopProcess(child, { closeObserved }),
|
|
1202
|
+
new Promise((_, reject) => {
|
|
1203
|
+
timer = setTimeout(() => {
|
|
1204
|
+
child.kill("SIGKILL");
|
|
1205
|
+
reject(new ACPFailure("cleanup"));
|
|
1206
|
+
}, Math.max(1, deadline - performance.now()));
|
|
1207
|
+
})
|
|
1208
|
+
]);
|
|
1209
|
+
while (performance.now() < deadline) {
|
|
1210
|
+
const state = await jobProbe(name, deadline - performance.now());
|
|
1211
|
+
if (state === 0)
|
|
1212
|
+
return;
|
|
1213
|
+
if (state !== 1)
|
|
1214
|
+
throw new ACPFailure("cleanup");
|
|
1215
|
+
await new Promise((resolve3) => setTimeout(resolve3, 20));
|
|
1216
|
+
}
|
|
1217
|
+
throw new ACPFailure("cleanup");
|
|
1218
|
+
} finally {
|
|
1219
|
+
clearTimeout(timer);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function stopProcess(child, options = {}) {
|
|
1223
|
+
if (process2.platform === "win32" && options.windowsJob)
|
|
1224
|
+
return stopWindowsJob(child, options.windowsJob, options.closeObserved);
|
|
1225
|
+
const exited = child.exitCode !== null || child.signalCode !== null;
|
|
1226
|
+
if (!child.pid || exited && !options.ownedTree)
|
|
1227
|
+
return;
|
|
1228
|
+
if (process2.platform === "win32" && exited && options.ownedTree)
|
|
1229
|
+
throw new ACPFailure("cleanup");
|
|
1230
|
+
const closed = options.closeObserved ? Promise.resolve() : new Promise((resolve3) => child.once("close", () => resolve3()));
|
|
1231
|
+
if (process2.platform === "win32") {
|
|
1232
|
+
await new Promise((resolve3, reject) => {
|
|
1233
|
+
const killer = spawn(
|
|
1234
|
+
"taskkill",
|
|
1235
|
+
["/PID", String(child.pid), "/T", "/F"],
|
|
1236
|
+
{ windowsHide: true, stdio: "ignore" }
|
|
1237
|
+
);
|
|
1238
|
+
killer.once("error", reject);
|
|
1239
|
+
killer.once("exit", (code) => code === 0 || !options.ownedTree && child.exitCode !== null ? resolve3() : reject(new ACPFailure("cleanup")));
|
|
1240
|
+
});
|
|
1241
|
+
} else {
|
|
1242
|
+
try {
|
|
1243
|
+
process2.kill(-child.pid, "SIGKILL");
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
if (error.code !== "ESRCH")
|
|
1246
|
+
throw new ACPFailure("cleanup");
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
await closed;
|
|
1250
|
+
}
|
|
1251
|
+
function baseEnvironment() {
|
|
1252
|
+
const output = {};
|
|
1253
|
+
for (const [key, value] of Object.entries(process2.env)) {
|
|
1254
|
+
if (value !== void 0 && /^(?:path|pathext|systemroot|windir|comspec|temp|tmp|lang|lc_all|ssl_cert_file|node_extra_ca_certs)$/i.test(
|
|
1255
|
+
key
|
|
1256
|
+
)) {
|
|
1257
|
+
output[key] = value;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return output;
|
|
1261
|
+
}
|
|
1262
|
+
function engineEnvironment() {
|
|
1263
|
+
const output = baseEnvironment();
|
|
1264
|
+
for (const [key, value] of Object.entries(process2.env)) {
|
|
1265
|
+
if (value !== void 0 && /^(?:HOME|USERPROFILE|APPDATA|LOCALAPPDATA|XDG_CONFIG_HOME|XDG_RUNTIME_DIR|DOCKER_HOST|DOCKER_CONTEXT|DOCKER_CONFIG|CONTAINER_HOST|CONTAINER_CONNECTION|SSH_AUTH_SOCK)$/i.test(
|
|
1266
|
+
key
|
|
1267
|
+
)) {
|
|
1268
|
+
output[key] = value;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return output;
|
|
1272
|
+
}
|
|
1273
|
+
async function execute(command, args, cwd, env, timeout = 12e4) {
|
|
1274
|
+
return await new Promise((resolve3, reject) => {
|
|
1275
|
+
const child = spawn(command, args, {
|
|
1276
|
+
cwd,
|
|
1277
|
+
env: {
|
|
1278
|
+
...command === "podman" || command === "docker" ? engineEnvironment() : baseEnvironment(),
|
|
1279
|
+
...env
|
|
1280
|
+
},
|
|
1281
|
+
shell: false,
|
|
1282
|
+
detached: process2.platform !== "win32",
|
|
1283
|
+
windowsHide: true,
|
|
1284
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1285
|
+
});
|
|
1286
|
+
let output = "";
|
|
1287
|
+
let failure;
|
|
1288
|
+
const stop = (error) => {
|
|
1289
|
+
failure ??= error;
|
|
1290
|
+
void stopProcess(child).catch((error2) => {
|
|
1291
|
+
failure = error2;
|
|
1292
|
+
});
|
|
1293
|
+
};
|
|
1294
|
+
const timer = setTimeout(() => stop(new ACPFailure("timeout")), timeout);
|
|
1295
|
+
child.stdout.on("data", (chunk) => {
|
|
1296
|
+
output += chunk.toString("utf8");
|
|
1297
|
+
if (Buffer.byteLength(output) > 8 * 1024 * 1024)
|
|
1298
|
+
stop(new ACPFailure("output"));
|
|
1299
|
+
});
|
|
1300
|
+
child.stderr.resume();
|
|
1301
|
+
child.once("error", (error) => {
|
|
1302
|
+
failure = new ACPFailure(
|
|
1303
|
+
command === "git" && error.code === "ENOENT" ? "gitMissing" : "runtime"
|
|
1304
|
+
);
|
|
1305
|
+
});
|
|
1306
|
+
child.once("close", (code) => {
|
|
1307
|
+
clearTimeout(timer);
|
|
1308
|
+
if (failure)
|
|
1309
|
+
reject(failure);
|
|
1310
|
+
else if (code !== 0)
|
|
1311
|
+
reject(new ACPFailure("failed", { exitCode: code }));
|
|
1312
|
+
else resolve3(output.trim());
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// src/acp/ai-acp-client.ts
|
|
1318
|
+
async function connectACP(transport, options = {}, inputLimits) {
|
|
1319
|
+
const limits = ACPConfigSchema.parse(inputLimits ?? {});
|
|
1320
|
+
const handlers = options.handlers ?? {};
|
|
1321
|
+
const capabilities = options.capabilities ?? {};
|
|
1322
|
+
const required2 = [];
|
|
1323
|
+
if (capabilities.fs?.readTextFile)
|
|
1324
|
+
required2.push(methods.client.fs.readTextFile);
|
|
1325
|
+
if (capabilities.fs?.writeTextFile)
|
|
1326
|
+
required2.push(methods.client.fs.writeTextFile);
|
|
1327
|
+
if (capabilities.terminal)
|
|
1328
|
+
required2.push(...Object.values(methods.client.terminal));
|
|
1329
|
+
if (capabilities.elicitation)
|
|
1330
|
+
required2.push(methods.client.elicitation.create);
|
|
1331
|
+
if (required2.some((name) => !Object.hasOwn(handlers, name))) {
|
|
1332
|
+
throw new ACPFailure("invalid");
|
|
1333
|
+
}
|
|
1334
|
+
if (options.signal?.aborted)
|
|
1335
|
+
throw new ACPFailure("cancelled");
|
|
1336
|
+
let stop = async () => {
|
|
1337
|
+
};
|
|
1338
|
+
let stream;
|
|
1339
|
+
if (transport.type === "stdio") {
|
|
1340
|
+
const job = process2.platform === "win32" ? windowsACPCommand(transport.command, transport.args ?? []) : void 0;
|
|
1341
|
+
const child = spawn(job ? "powershell.exe" : transport.command, job?.args ?? transport.args ?? [], {
|
|
1342
|
+
cwd: transport.cwd,
|
|
1343
|
+
env: { ...baseEnvironment(), ...transport.env, ...job?.env },
|
|
1344
|
+
detached: process2.platform !== "win32",
|
|
1345
|
+
shell: false,
|
|
1346
|
+
windowsHide: true,
|
|
1347
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1348
|
+
});
|
|
1349
|
+
let childClosed = false;
|
|
1350
|
+
child.once("close", () => {
|
|
1351
|
+
childClosed = true;
|
|
1352
|
+
});
|
|
1353
|
+
child.stderr.resume();
|
|
1354
|
+
await new Promise((resolve3, reject) => {
|
|
1355
|
+
child.once("spawn", resolve3);
|
|
1356
|
+
child.once("error", () => reject(new ACPFailure("runtime")));
|
|
1357
|
+
});
|
|
1358
|
+
stop = () => stopProcess(child, { ownedTree: true, closeObserved: childClosed, windowsJob: job?.name });
|
|
1359
|
+
stream = ndJsonStream(
|
|
1360
|
+
Writable.toWeb(child.stdin),
|
|
1361
|
+
Readable.toWeb(child.stdout),
|
|
1362
|
+
{ maxMessageBytes: limits.maxContentBytes }
|
|
1363
|
+
);
|
|
1364
|
+
} else if (transport.type === "http") {
|
|
1365
|
+
stream = createHttpStream(transport.url, {
|
|
1366
|
+
headers: transport.headers,
|
|
1367
|
+
maxMessageBytes: limits.maxContentBytes
|
|
1368
|
+
});
|
|
1369
|
+
} else {
|
|
1370
|
+
class BoundedWebSocket extends WebSocket {
|
|
1371
|
+
constructor(url, protocols, options2) {
|
|
1372
|
+
super(url, protocols, {
|
|
1373
|
+
...options2,
|
|
1374
|
+
maxPayload: limits.maxContentBytes
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
stream = createWebSocketStream(transport.url, {
|
|
1379
|
+
headers: transport.headers,
|
|
1380
|
+
WebSocket: BoundedWebSocket
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
const requests = /* @__PURE__ */ new Set();
|
|
1384
|
+
const originalWriter = stream.writable.getWriter();
|
|
1385
|
+
const guardedStream = {
|
|
1386
|
+
writable: new WritableStream({
|
|
1387
|
+
async write(message) {
|
|
1388
|
+
if ("method" in message && "id" in message && message.id !== void 0 && message.id !== null) {
|
|
1389
|
+
requests.add(message.id);
|
|
1390
|
+
}
|
|
1391
|
+
await originalWriter.write(message);
|
|
1392
|
+
},
|
|
1393
|
+
close: () => originalWriter.close(),
|
|
1394
|
+
abort: (reason) => originalWriter.abort(reason)
|
|
1395
|
+
}),
|
|
1396
|
+
readable: stream.readable.pipeThrough(
|
|
1397
|
+
new TransformStream({
|
|
1398
|
+
transform(message, controller) {
|
|
1399
|
+
if (!("method" in message) && "id" in message && message.id !== void 0 && message.id !== null) {
|
|
1400
|
+
requests.delete(message.id);
|
|
1401
|
+
}
|
|
1402
|
+
controller.enqueue(message);
|
|
1403
|
+
}
|
|
1404
|
+
})
|
|
1405
|
+
)
|
|
1406
|
+
};
|
|
1407
|
+
let sessionId;
|
|
1408
|
+
let sessionOpening = false;
|
|
1409
|
+
let closed = false;
|
|
1410
|
+
let failure;
|
|
1411
|
+
let closePromise;
|
|
1412
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1413
|
+
const app = client({ name: "hai-ai" });
|
|
1414
|
+
app.onNotification(methods.client.session.update, ({ params }) => {
|
|
1415
|
+
if (sessionId && params.sessionId !== sessionId)
|
|
1416
|
+
return;
|
|
1417
|
+
try {
|
|
1418
|
+
options.onEvent?.(params);
|
|
1419
|
+
for (const listener of listeners) listener(params);
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
failure = error;
|
|
1422
|
+
void close().catch(() => {
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
function owned(id) {
|
|
1427
|
+
if (!sessionId || id !== sessionId)
|
|
1428
|
+
throw new ACPFailure("missing");
|
|
1429
|
+
}
|
|
1430
|
+
app.onRequest(methods.client.session.requestPermission, async (context) => {
|
|
1431
|
+
owned(context.params.sessionId);
|
|
1432
|
+
const handler = handlers["session/request_permission"];
|
|
1433
|
+
if (!handler)
|
|
1434
|
+
return { outcome: { outcome: "cancelled" } };
|
|
1435
|
+
return handler(context);
|
|
1436
|
+
});
|
|
1437
|
+
app.onRequest(methods.client.fs.readTextFile, async (context) => {
|
|
1438
|
+
owned(context.params.sessionId);
|
|
1439
|
+
const handler = handlers["fs/read_text_file"];
|
|
1440
|
+
if (!handler)
|
|
1441
|
+
throw new ACPFailure("unsupported");
|
|
1442
|
+
return handler(context);
|
|
1443
|
+
});
|
|
1444
|
+
app.onRequest(methods.client.fs.writeTextFile, async (context) => {
|
|
1445
|
+
owned(context.params.sessionId);
|
|
1446
|
+
const handler = handlers["fs/write_text_file"];
|
|
1447
|
+
if (!handler)
|
|
1448
|
+
throw new ACPFailure("unsupported");
|
|
1449
|
+
return handler(context);
|
|
1450
|
+
});
|
|
1451
|
+
app.onRequest(methods.client.terminal.create, async (context) => {
|
|
1452
|
+
owned(context.params.sessionId);
|
|
1453
|
+
const handler = handlers["terminal/create"];
|
|
1454
|
+
if (!handler)
|
|
1455
|
+
throw new ACPFailure("unsupported");
|
|
1456
|
+
return handler(context);
|
|
1457
|
+
});
|
|
1458
|
+
app.onRequest(methods.client.terminal.output, async (context) => {
|
|
1459
|
+
owned(context.params.sessionId);
|
|
1460
|
+
const handler = handlers["terminal/output"];
|
|
1461
|
+
if (!handler)
|
|
1462
|
+
throw new ACPFailure("unsupported");
|
|
1463
|
+
return handler(context);
|
|
1464
|
+
});
|
|
1465
|
+
app.onRequest(methods.client.terminal.release, async (context) => {
|
|
1466
|
+
owned(context.params.sessionId);
|
|
1467
|
+
const handler = handlers["terminal/release"];
|
|
1468
|
+
if (!handler)
|
|
1469
|
+
throw new ACPFailure("unsupported");
|
|
1470
|
+
return handler(context);
|
|
1471
|
+
});
|
|
1472
|
+
app.onRequest(methods.client.terminal.waitForExit, async (context) => {
|
|
1473
|
+
owned(context.params.sessionId);
|
|
1474
|
+
const handler = handlers["terminal/wait_for_exit"];
|
|
1475
|
+
if (!handler)
|
|
1476
|
+
throw new ACPFailure("unsupported");
|
|
1477
|
+
return handler(context);
|
|
1478
|
+
});
|
|
1479
|
+
app.onRequest(methods.client.terminal.kill, async (context) => {
|
|
1480
|
+
owned(context.params.sessionId);
|
|
1481
|
+
const handler = handlers["terminal/kill"];
|
|
1482
|
+
if (!handler)
|
|
1483
|
+
throw new ACPFailure("unsupported");
|
|
1484
|
+
return handler(context);
|
|
1485
|
+
});
|
|
1486
|
+
app.onRequest(methods.client.elicitation.create, async (context) => {
|
|
1487
|
+
if ("sessionId" in context.params && typeof context.params.sessionId === "string") {
|
|
1488
|
+
owned(context.params.sessionId);
|
|
1489
|
+
} else if (!("requestId" in context.params) || typeof context.params.requestId !== "string" && typeof context.params.requestId !== "number" || !requests.has(context.params.requestId)) {
|
|
1490
|
+
throw new ACPFailure("missing");
|
|
1491
|
+
}
|
|
1492
|
+
const handler = handlers["elicitation/create"];
|
|
1493
|
+
if (!handler)
|
|
1494
|
+
throw new ACPFailure("unsupported");
|
|
1495
|
+
return handler(context);
|
|
1496
|
+
});
|
|
1497
|
+
const connection = app.connect(guardedStream);
|
|
1498
|
+
function close() {
|
|
1499
|
+
closed = true;
|
|
1500
|
+
if (!closePromise) {
|
|
1501
|
+
const task = (async () => {
|
|
1502
|
+
try {
|
|
1503
|
+
await stop();
|
|
1504
|
+
} finally {
|
|
1505
|
+
connection.close();
|
|
1506
|
+
}
|
|
1507
|
+
})();
|
|
1508
|
+
closePromise = task;
|
|
1509
|
+
void task.catch(() => {
|
|
1510
|
+
if (closePromise === task)
|
|
1511
|
+
closePromise = void 0;
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
return closePromise;
|
|
1515
|
+
}
|
|
1516
|
+
async function request(operation, timeout = limits.requestTimeout, signal, prompt = false, isCancelledResponse) {
|
|
1517
|
+
if (closed)
|
|
1518
|
+
throw new ACPFailure("closed");
|
|
1519
|
+
if (signal?.aborted)
|
|
1520
|
+
throw new ACPFailure("cancelled");
|
|
1521
|
+
let timer;
|
|
1522
|
+
let abort = () => {
|
|
1523
|
+
};
|
|
1524
|
+
let reason;
|
|
1525
|
+
const deadline = new Promise((_, reject) => {
|
|
1526
|
+
const fail = (kind) => {
|
|
1527
|
+
if (reason)
|
|
1528
|
+
return;
|
|
1529
|
+
reason = new ACPFailure(kind);
|
|
1530
|
+
if (prompt && sessionId) {
|
|
1531
|
+
void connection.agent.notify(methods.agent.session.cancel, { sessionId }).catch(() => {
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
clearTimeout(timer);
|
|
1535
|
+
timer = setTimeout(
|
|
1536
|
+
() => {
|
|
1537
|
+
void close().then(() => reject(reason), reject);
|
|
1538
|
+
},
|
|
1539
|
+
prompt ? limits.cancelTimeout : 0
|
|
1540
|
+
);
|
|
1541
|
+
};
|
|
1542
|
+
timer = setTimeout(fail, timeout, "timeout");
|
|
1543
|
+
abort = () => fail("cancelled");
|
|
1544
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1545
|
+
});
|
|
1546
|
+
try {
|
|
1547
|
+
const output = await Promise.race([operation(), deadline]);
|
|
1548
|
+
if (failure)
|
|
1549
|
+
throw failure;
|
|
1550
|
+
if (reason) {
|
|
1551
|
+
if (!(reason.kind === "cancelled" && prompt && isCancelledResponse?.(output)))
|
|
1552
|
+
await close();
|
|
1553
|
+
throw reason;
|
|
1554
|
+
}
|
|
1555
|
+
return output;
|
|
1556
|
+
} catch (error) {
|
|
1557
|
+
if (failure)
|
|
1558
|
+
throw failure;
|
|
1559
|
+
if (reason)
|
|
1560
|
+
throw reason;
|
|
1561
|
+
throw error;
|
|
1562
|
+
} finally {
|
|
1563
|
+
clearTimeout(timer);
|
|
1564
|
+
signal?.removeEventListener("abort", abort);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
try {
|
|
1568
|
+
let checkMcp2 = function(servers) {
|
|
1569
|
+
for (const server of servers) {
|
|
1570
|
+
if ("type" in server && server.type === "http" && !agentCapabilities.mcpCapabilities?.http) {
|
|
1571
|
+
throw new ACPFailure("unsupported");
|
|
1572
|
+
}
|
|
1573
|
+
if ("type" in server && server.type === "sse" && !agentCapabilities.mcpCapabilities?.sse) {
|
|
1574
|
+
throw new ACPFailure("unsupported");
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
var checkMcp = checkMcp2;
|
|
1579
|
+
const initialized = await request(
|
|
1580
|
+
() => connection.agent.request(methods.agent.initialize, {
|
|
1581
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1582
|
+
clientCapabilities: capabilities
|
|
1583
|
+
}),
|
|
1584
|
+
limits.requestTimeout,
|
|
1585
|
+
options.signal
|
|
1586
|
+
);
|
|
1587
|
+
if (initialized.protocolVersion !== PROTOCOL_VERSION)
|
|
1588
|
+
throw new ACPFailure("unsupported");
|
|
1589
|
+
const agentCapabilities = initialized.agentCapabilities ?? {};
|
|
1590
|
+
let activePrompt;
|
|
1591
|
+
async function prompt(content, signal) {
|
|
1592
|
+
if (activePrompt)
|
|
1593
|
+
throw new ACPFailure("busy");
|
|
1594
|
+
if (!sessionId)
|
|
1595
|
+
throw new ACPFailure("missing");
|
|
1596
|
+
if (Buffer.byteLength(JSON.stringify(content)) > limits.maxContentBytes)
|
|
1597
|
+
throw new ACPFailure("output");
|
|
1598
|
+
for (const item of content) {
|
|
1599
|
+
if (item.type === "image" && !agentCapabilities.promptCapabilities?.image || item.type === "audio" && !agentCapabilities.promptCapabilities?.audio || item.type === "resource" && !agentCapabilities.promptCapabilities?.embeddedContext) {
|
|
1600
|
+
throw new ACPFailure("unsupported");
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
const events = [];
|
|
1604
|
+
let bytes = 0;
|
|
1605
|
+
let text = "";
|
|
1606
|
+
let messageId;
|
|
1607
|
+
const listener = (event) => {
|
|
1608
|
+
bytes += Buffer.byteLength(JSON.stringify(event));
|
|
1609
|
+
if (events.length >= limits.maxPendingEvents || bytes > limits.maxContentBytes) {
|
|
1610
|
+
throw new ACPFailure("output");
|
|
1611
|
+
}
|
|
1612
|
+
events.push(event);
|
|
1613
|
+
if (event.update.sessionUpdate === "agent_message_chunk" && event.update.content.type === "text") {
|
|
1614
|
+
if (event.update.messageId && event.update.messageId !== messageId)
|
|
1615
|
+
text = "";
|
|
1616
|
+
messageId = event.update.messageId;
|
|
1617
|
+
text += event.update.content.text;
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
listeners.add(listener);
|
|
1621
|
+
activePrompt = new AbortController();
|
|
1622
|
+
try {
|
|
1623
|
+
const response = await request(
|
|
1624
|
+
() => connection.agent.request(methods.agent.session.prompt, {
|
|
1625
|
+
sessionId,
|
|
1626
|
+
prompt: content
|
|
1627
|
+
}),
|
|
1628
|
+
limits.promptTimeout,
|
|
1629
|
+
signal ? AbortSignal.any([activePrompt.signal, signal]) : activePrompt.signal,
|
|
1630
|
+
true,
|
|
1631
|
+
(output) => output.stopReason === "cancelled"
|
|
1632
|
+
);
|
|
1633
|
+
if (response.stopReason === "cancelled")
|
|
1634
|
+
throw new ACPFailure("cancelled");
|
|
1635
|
+
return { ...response, text, events };
|
|
1636
|
+
} finally {
|
|
1637
|
+
listeners.delete(listener);
|
|
1638
|
+
activePrompt = void 0;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return {
|
|
1642
|
+
initialized,
|
|
1643
|
+
/**
|
|
1644
|
+
* 关闭传输并终止拥有的进程树,可重复调用。
|
|
1645
|
+
* @returns 清理完成的 Promise;清理失败抛错。
|
|
1646
|
+
* @example
|
|
1647
|
+
* ```ts
|
|
1648
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
1649
|
+
* await connection.close()
|
|
1650
|
+
* ```
|
|
1651
|
+
*/
|
|
1652
|
+
close,
|
|
1653
|
+
/**
|
|
1654
|
+
* 执行单轮原生请求并收集有限事件。
|
|
1655
|
+
* @param content - 原生内容块。
|
|
1656
|
+
* @param signal - 本轮取消信号。
|
|
1657
|
+
* @returns 最终文本、停止原因及事件;业务边界负责转换异常。
|
|
1658
|
+
* @example
|
|
1659
|
+
* ```ts
|
|
1660
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
1661
|
+
* const response = await connection.prompt([{ type: 'text', text: '检查工程' }])
|
|
1662
|
+
* ```
|
|
1663
|
+
*/
|
|
1664
|
+
prompt,
|
|
1665
|
+
/**
|
|
1666
|
+
* 订阅会话事件;返回取消订阅函数。
|
|
1667
|
+
* @param listener - 消费原生事件的回调;抛错会关闭连接。
|
|
1668
|
+
* @returns 取消订阅函数。
|
|
1669
|
+
* @example
|
|
1670
|
+
* ```ts
|
|
1671
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1672
|
+
* const unsubscribe = connection.subscribe(event => consume(event)); unsubscribe()
|
|
1673
|
+
* ```
|
|
1674
|
+
*/
|
|
1675
|
+
subscribe(listener) {
|
|
1676
|
+
listeners.add(listener);
|
|
1677
|
+
return () => listeners.delete(listener);
|
|
1678
|
+
},
|
|
1679
|
+
/**
|
|
1680
|
+
* 发送原生取消通知并触发正在执行轮次的取消期限。
|
|
1681
|
+
* @returns 通知发送后的 Promise;本轮终止由 prompt 的取消期限保证。
|
|
1682
|
+
* @example
|
|
1683
|
+
* ```ts
|
|
1684
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1685
|
+
* await connection.cancel()
|
|
1686
|
+
* ```
|
|
1687
|
+
*/
|
|
1688
|
+
cancel: async () => {
|
|
1689
|
+
activePrompt?.abort();
|
|
1690
|
+
if (sessionId && !closed) {
|
|
1691
|
+
await connection.agent.notify(methods.agent.session.cancel, {
|
|
1692
|
+
sessionId
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
/**
|
|
1697
|
+
* 选择握手 authMethods 中的认证方法,凭据由原生进程或远程服务提供。
|
|
1698
|
+
* @param methodId - 对端握手声明的认证方法。
|
|
1699
|
+
* @returns 原生认证响应。
|
|
1700
|
+
* @example
|
|
1701
|
+
* ```ts
|
|
1702
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1703
|
+
* await connection.authenticate('api-key')
|
|
1704
|
+
* ```
|
|
1705
|
+
*/
|
|
1706
|
+
authenticate: (methodId) => request(
|
|
1707
|
+
() => connection.agent.request(methods.agent.authenticate, { methodId })
|
|
1708
|
+
),
|
|
1709
|
+
/**
|
|
1710
|
+
* 创建此连接唯一的原生会话;MCP 类型必须由智能体声明支持。
|
|
1711
|
+
* @param cwd - 对端可见的绝对工作目录。
|
|
1712
|
+
* @param mcpServers - 原生 MCP 定义,默认空列表。
|
|
1713
|
+
* @returns 包含原生 sessionId 的新建响应。
|
|
1714
|
+
* @example
|
|
1715
|
+
* ```ts
|
|
1716
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1717
|
+
* await connection.createSession('/srv/project', [])
|
|
1718
|
+
* ```
|
|
1719
|
+
*/
|
|
1720
|
+
async createSession(cwd, mcpServers = []) {
|
|
1721
|
+
if (sessionId || sessionOpening)
|
|
1722
|
+
throw new ACPFailure("busy");
|
|
1723
|
+
sessionOpening = true;
|
|
1724
|
+
try {
|
|
1725
|
+
checkMcp2(mcpServers);
|
|
1726
|
+
const response = await request(
|
|
1727
|
+
() => connection.agent.request(methods.agent.session.new, {
|
|
1728
|
+
cwd,
|
|
1729
|
+
mcpServers
|
|
1730
|
+
})
|
|
1731
|
+
);
|
|
1732
|
+
sessionId = response.sessionId;
|
|
1733
|
+
return response;
|
|
1734
|
+
} finally {
|
|
1735
|
+
sessionOpening = false;
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1738
|
+
/**
|
|
1739
|
+
* 优先调用原生 resume,否则使用已声明的 load;均不支持则失败。
|
|
1740
|
+
* @param id - 原生会话 ID。
|
|
1741
|
+
* @param cwd - 原会话绝对工作目录。
|
|
1742
|
+
* @param mcpServers - 当前原生 MCP 定义。
|
|
1743
|
+
* @returns 恢复或加载响应;能力缺失时抛错。
|
|
1744
|
+
* @example
|
|
1745
|
+
* ```ts
|
|
1746
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1747
|
+
* await connection.resumeSession('native-id', '/srv/project')
|
|
1748
|
+
* ```
|
|
1749
|
+
*/
|
|
1750
|
+
async resumeSession(id, cwd, mcpServers = []) {
|
|
1751
|
+
if (sessionId || sessionOpening)
|
|
1752
|
+
throw new ACPFailure("busy");
|
|
1753
|
+
checkMcp2(mcpServers);
|
|
1754
|
+
sessionId = id;
|
|
1755
|
+
if (agentCapabilities.sessionCapabilities?.resume) {
|
|
1756
|
+
return request(
|
|
1757
|
+
() => connection.agent.request(methods.agent.session.resume, {
|
|
1758
|
+
sessionId: id,
|
|
1759
|
+
cwd,
|
|
1760
|
+
mcpServers
|
|
1761
|
+
})
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
if (agentCapabilities.loadSession) {
|
|
1765
|
+
return request(
|
|
1766
|
+
() => connection.agent.request(methods.agent.session.load, {
|
|
1767
|
+
sessionId: id,
|
|
1768
|
+
cwd,
|
|
1769
|
+
mcpServers
|
|
1770
|
+
})
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
throw new ACPFailure("unsupported");
|
|
1774
|
+
},
|
|
1775
|
+
/**
|
|
1776
|
+
* 按智能体原生游标列出会话;未声明 list 能力则失败。
|
|
1777
|
+
* @param cursor - 可选分页游标。
|
|
1778
|
+
* @returns 原生分页会话列表。
|
|
1779
|
+
* @example
|
|
1780
|
+
* ```ts
|
|
1781
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1782
|
+
* const page = await connection.listSessions()
|
|
1783
|
+
* ```
|
|
1784
|
+
*/
|
|
1785
|
+
async listSessions(cursor) {
|
|
1786
|
+
if (!agentCapabilities.sessionCapabilities?.list)
|
|
1787
|
+
throw new ACPFailure("unsupported");
|
|
1788
|
+
return request(
|
|
1789
|
+
() => connection.agent.request(methods.agent.session.list, { cursor })
|
|
1790
|
+
);
|
|
1791
|
+
},
|
|
1792
|
+
/**
|
|
1793
|
+
* 要求远端删除指定原生会话;不代替框架工作区清理。
|
|
1794
|
+
* @param id - 原生会话 ID。
|
|
1795
|
+
* @returns 原生删除响应。
|
|
1796
|
+
* @example
|
|
1797
|
+
* ```ts
|
|
1798
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1799
|
+
* await connection.deleteSession('native-id')
|
|
1800
|
+
* ```
|
|
1801
|
+
*/
|
|
1802
|
+
async deleteSession(id) {
|
|
1803
|
+
if (!agentCapabilities.sessionCapabilities?.delete)
|
|
1804
|
+
throw new ACPFailure("unsupported");
|
|
1805
|
+
return request(
|
|
1806
|
+
() => connection.agent.request(methods.agent.session.delete, {
|
|
1807
|
+
sessionId: id
|
|
1808
|
+
})
|
|
1809
|
+
);
|
|
1810
|
+
},
|
|
1811
|
+
/**
|
|
1812
|
+
* 若远端声明 close 能力则关闭原生会话;进程停止由 close 负责。
|
|
1813
|
+
* @returns 原生关闭完成的 Promise;未声明能力时无需请求。
|
|
1814
|
+
* @example
|
|
1815
|
+
* ```ts
|
|
1816
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1817
|
+
* await connection.closeSession()
|
|
1818
|
+
* ```
|
|
1819
|
+
*/
|
|
1820
|
+
async closeSession() {
|
|
1821
|
+
if (!closed && sessionId && agentCapabilities.sessionCapabilities?.close) {
|
|
1822
|
+
await request(
|
|
1823
|
+
() => connection.agent.request(methods.agent.session.close, {
|
|
1824
|
+
sessionId
|
|
1825
|
+
})
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
},
|
|
1829
|
+
/**
|
|
1830
|
+
* 给当前原生会话设置智能体公布的模式。
|
|
1831
|
+
* @param modeId - 原生模式 ID。
|
|
1832
|
+
* @returns 原生模式响应。
|
|
1833
|
+
* @example
|
|
1834
|
+
* ```ts
|
|
1835
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1836
|
+
* await connection.setMode('code')
|
|
1837
|
+
* ```
|
|
1838
|
+
*/
|
|
1839
|
+
setMode: (modeId) => request(
|
|
1840
|
+
() => connection.agent.request(methods.agent.session.setMode, {
|
|
1841
|
+
sessionId,
|
|
1842
|
+
modeId
|
|
1843
|
+
})
|
|
1844
|
+
),
|
|
1845
|
+
/**
|
|
1846
|
+
* 给当前原生会话设置智能体公布的配置选项。
|
|
1847
|
+
* @param configId - 原生配置选项 ID。
|
|
1848
|
+
* @param value - 智能体允许的选项值。
|
|
1849
|
+
* @returns 原生配置响应。
|
|
1850
|
+
* @example
|
|
1851
|
+
* ```ts
|
|
1852
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1853
|
+
* await connection.setConfigOption('model', 'coding')
|
|
1854
|
+
* ```
|
|
1855
|
+
*/
|
|
1856
|
+
setConfigOption: (configId, value) => request(
|
|
1857
|
+
() => connection.agent.request(methods.agent.session.setConfigOption, {
|
|
1858
|
+
sessionId,
|
|
1859
|
+
configId,
|
|
1860
|
+
value
|
|
1861
|
+
})
|
|
1862
|
+
)
|
|
1863
|
+
};
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
await close();
|
|
1866
|
+
throw error;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// src/acp/ai-acp-git.ts
|
|
1871
|
+
function createGitOperations(workspace, runtime) {
|
|
1872
|
+
const git = (cwd, args, env) => runtime.run("git", ["-C", cwd, ...args], void 0, {
|
|
1873
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1874
|
+
...env
|
|
1875
|
+
});
|
|
1876
|
+
const source = (id) => runtime.path(`main/${id}`);
|
|
1877
|
+
async function ready() {
|
|
1878
|
+
if (!workspace.repositories.length)
|
|
1879
|
+
return;
|
|
1880
|
+
try {
|
|
1881
|
+
await runtime.run("git", ["--version"]);
|
|
1882
|
+
} catch {
|
|
1883
|
+
throw new ACPFailure("gitMissing");
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
async function clean(path) {
|
|
1887
|
+
if (await git(path, ["status", "--porcelain"]))
|
|
1888
|
+
throw new ACPFailure("conflict");
|
|
1889
|
+
}
|
|
1890
|
+
async function supportedContent(path) {
|
|
1891
|
+
const index = await git(path, ["ls-files", "--stage", "-z"]);
|
|
1892
|
+
if (index.split("\0").some((entry) => entry.startsWith("160000 ")))
|
|
1893
|
+
throw new ACPFailure("unsupported");
|
|
1894
|
+
const attributes = await git(path, ["ls-files", "-z", "--", ".gitattributes", ":(glob)**/.gitattributes"]);
|
|
1895
|
+
for (const file of attributes.split("\0").filter(Boolean)) {
|
|
1896
|
+
const content = await git(path, ["show", `HEAD:${file}`]);
|
|
1897
|
+
if (content.split("\n").some((line) => !line.trimStart().startsWith("#") && /\bfilter=lfs\b/.test(line)))
|
|
1898
|
+
throw new ACPFailure("unsupported");
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
async function checked(repo, path = source(repo.id), branch = repo.mainBranch) {
|
|
1902
|
+
if (await git(path, ["branch", "--show-current"]) !== branch)
|
|
1903
|
+
throw new ACPFailure("conflict");
|
|
1904
|
+
if (await git(path, ["remote", "get-url", repo.remote]) !== repo.url)
|
|
1905
|
+
throw new ACPFailure("conflict");
|
|
1906
|
+
await runtime.file("exists", path);
|
|
1907
|
+
}
|
|
1908
|
+
async function reports(operation) {
|
|
1909
|
+
await ready();
|
|
1910
|
+
const output = [];
|
|
1911
|
+
for (const repo of workspace.repositories) {
|
|
1912
|
+
const report = {
|
|
1913
|
+
repositoryId: repo.id,
|
|
1914
|
+
success: false,
|
|
1915
|
+
phase: "check"
|
|
1916
|
+
};
|
|
1917
|
+
try {
|
|
1918
|
+
await operation(repo, report);
|
|
1919
|
+
report.success = true;
|
|
1920
|
+
report.phase = "done";
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
report.error = error instanceof ACPFailure ? error.kind : "failed";
|
|
1923
|
+
}
|
|
1924
|
+
output.push(report);
|
|
1925
|
+
}
|
|
1926
|
+
if (output.some((item) => !item.success))
|
|
1927
|
+
throw new ACPFailure("partial", { reports: output });
|
|
1928
|
+
return output;
|
|
1929
|
+
}
|
|
1930
|
+
return {
|
|
1931
|
+
/**
|
|
1932
|
+
* 准备工程主目录,只克隆缺失仓库,不覆盖已有内容。
|
|
1933
|
+
* @returns main 的绝对路径;失败保留已创建文件。
|
|
1934
|
+
* @example
|
|
1935
|
+
* ```ts
|
|
1936
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
1937
|
+
* const main = await git.prepare()
|
|
1938
|
+
* ```
|
|
1939
|
+
*/
|
|
1940
|
+
async prepare() {
|
|
1941
|
+
await ready();
|
|
1942
|
+
await runtime.file("mkdir", "main/files");
|
|
1943
|
+
for (const repo of workspace.repositories) {
|
|
1944
|
+
await git(runtime.root, [
|
|
1945
|
+
"check-ref-format",
|
|
1946
|
+
"--branch",
|
|
1947
|
+
repo.mainBranch
|
|
1948
|
+
]);
|
|
1949
|
+
if (await runtime.file("exists", `main/${repo.id}`) === "false") {
|
|
1950
|
+
await git(runtime.root, [
|
|
1951
|
+
"clone",
|
|
1952
|
+
"--origin",
|
|
1953
|
+
repo.remote,
|
|
1954
|
+
"--branch",
|
|
1955
|
+
repo.mainBranch,
|
|
1956
|
+
"--",
|
|
1957
|
+
repo.url,
|
|
1958
|
+
source(repo.id)
|
|
1959
|
+
]);
|
|
1960
|
+
}
|
|
1961
|
+
await checked(repo);
|
|
1962
|
+
await supportedContent(source(repo.id));
|
|
1963
|
+
}
|
|
1964
|
+
return runtime.path("main");
|
|
1965
|
+
},
|
|
1966
|
+
/**
|
|
1967
|
+
* 从每个工程本地主分支创建独立工作树并复制普通文件。
|
|
1968
|
+
* @param sessionId - 已校验的框架会话 ID。
|
|
1969
|
+
* @param branch - 所有工程使用的同名新分支。
|
|
1970
|
+
* @returns 独立会话目录;中途失败不回滚已有工作树。
|
|
1971
|
+
* @example
|
|
1972
|
+
* ```ts
|
|
1973
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
1974
|
+
* const directory = await git.createWorktree('task-1', 'acp/task-1')
|
|
1975
|
+
* ```
|
|
1976
|
+
*/
|
|
1977
|
+
async createWorktree(sessionId, branch) {
|
|
1978
|
+
await ready();
|
|
1979
|
+
await runtime.file("mkdir", `sessions/${sessionId}`);
|
|
1980
|
+
for (const repo of workspace.repositories) {
|
|
1981
|
+
await checked(repo);
|
|
1982
|
+
await supportedContent(source(repo.id));
|
|
1983
|
+
await git(source(repo.id), ["check-ref-format", "--branch", branch]);
|
|
1984
|
+
await git(source(repo.id), [
|
|
1985
|
+
"worktree",
|
|
1986
|
+
"add",
|
|
1987
|
+
"-b",
|
|
1988
|
+
branch,
|
|
1989
|
+
runtime.path(`sessions/${sessionId}/${repo.id}`),
|
|
1990
|
+
`refs/heads/${repo.mainBranch}`
|
|
1991
|
+
]);
|
|
1992
|
+
}
|
|
1993
|
+
await runtime.file("copy", "main/files", `sessions/${sessionId}/files`);
|
|
1994
|
+
return runtime.path(`sessions/${sessionId}`);
|
|
1995
|
+
},
|
|
1996
|
+
/**
|
|
1997
|
+
* 更新各工程主分支,只允许快进合并。
|
|
1998
|
+
* @returns 每个工程的更新报告;部分失败抛出携带 reports 的异常。
|
|
1999
|
+
* @example
|
|
2000
|
+
* ```ts
|
|
2001
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2002
|
+
* const reports = await git.updateMain()
|
|
2003
|
+
* ```
|
|
2004
|
+
*/
|
|
2005
|
+
updateMain: () => reports(async (repo, report) => {
|
|
2006
|
+
const path = source(repo.id);
|
|
2007
|
+
await checked(repo);
|
|
2008
|
+
await clean(path);
|
|
2009
|
+
report.phase = "fetch";
|
|
2010
|
+
await git(path, [
|
|
2011
|
+
"fetch",
|
|
2012
|
+
"--",
|
|
2013
|
+
repo.remote,
|
|
2014
|
+
`refs/heads/${repo.mainBranch}`
|
|
2015
|
+
]);
|
|
2016
|
+
report.phase = "merge";
|
|
2017
|
+
await git(path, ["merge", "--ff-only", "FETCH_HEAD"]);
|
|
2018
|
+
report.commit = await git(path, ["rev-parse", "HEAD"]);
|
|
2019
|
+
}),
|
|
2020
|
+
/**
|
|
2021
|
+
* 逐仓提交会话修改并发布,禁止强推。
|
|
2022
|
+
* @param sessionId - 已登记的独立会话 ID。
|
|
2023
|
+
* @param branch - 会话工作树分支名。
|
|
2024
|
+
* @param request - 经过 schema 校验的模式、提交说明及作者。
|
|
2025
|
+
* @returns 各仓发布报告;部分失败保留已提交和已推送的结果。
|
|
2026
|
+
* @example
|
|
2027
|
+
* ```ts
|
|
2028
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2029
|
+
* const reports = await git.publish('task-1', 'acp/task-1', { mode: 'push-branch', message: '完成任务', author: { name: 'Developer', email: 'dev@example.com' } })
|
|
2030
|
+
* ```
|
|
2031
|
+
*/
|
|
2032
|
+
publish: (sessionId, branch, request) => reports(async (repo, report) => {
|
|
2033
|
+
const session = runtime.path(`sessions/${sessionId}/${repo.id}`);
|
|
2034
|
+
const main = source(repo.id);
|
|
2035
|
+
const author = {
|
|
2036
|
+
GIT_AUTHOR_NAME: request.author.name,
|
|
2037
|
+
GIT_AUTHOR_EMAIL: request.author.email,
|
|
2038
|
+
GIT_COMMITTER_NAME: request.author.name,
|
|
2039
|
+
GIT_COMMITTER_EMAIL: request.author.email
|
|
2040
|
+
};
|
|
2041
|
+
await checked(repo, session, branch);
|
|
2042
|
+
if (request.mode === "merge-main") {
|
|
2043
|
+
await checked(repo);
|
|
2044
|
+
await clean(main);
|
|
2045
|
+
}
|
|
2046
|
+
report.phase = "commit";
|
|
2047
|
+
if (await git(session, ["status", "--porcelain"])) {
|
|
2048
|
+
await git(session, ["add", "--all"]);
|
|
2049
|
+
await git(session, ["commit", "-m", request.message], author);
|
|
2050
|
+
}
|
|
2051
|
+
report.commit = await git(session, ["rev-parse", "HEAD"]);
|
|
2052
|
+
let target = session;
|
|
2053
|
+
let targetBranch = branch;
|
|
2054
|
+
if (request.mode === "merge-main") {
|
|
2055
|
+
report.phase = "fetch";
|
|
2056
|
+
await git(main, [
|
|
2057
|
+
"fetch",
|
|
2058
|
+
"--",
|
|
2059
|
+
repo.remote,
|
|
2060
|
+
`refs/heads/${repo.mainBranch}`
|
|
2061
|
+
]);
|
|
2062
|
+
report.phase = "merge";
|
|
2063
|
+
const remoteHead = await git(main, ["rev-parse", "FETCH_HEAD"]);
|
|
2064
|
+
const localHead = await git(main, ["rev-parse", "HEAD"]);
|
|
2065
|
+
const base = await git(main, ["merge-base", "HEAD", "FETCH_HEAD"]);
|
|
2066
|
+
if (base === localHead)
|
|
2067
|
+
await git(main, ["merge", "--ff-only", "FETCH_HEAD"]);
|
|
2068
|
+
else if (base !== remoteHead)
|
|
2069
|
+
throw new ACPFailure("conflict");
|
|
2070
|
+
await git(main, ["merge", "--no-edit", "--", branch], author);
|
|
2071
|
+
target = main;
|
|
2072
|
+
targetBranch = repo.mainBranch;
|
|
2073
|
+
}
|
|
2074
|
+
report.phase = "push";
|
|
2075
|
+
await git(target, [
|
|
2076
|
+
"push",
|
|
2077
|
+
"--",
|
|
2078
|
+
repo.remote,
|
|
2079
|
+
`HEAD:refs/heads/${targetBranch}`
|
|
2080
|
+
]);
|
|
2081
|
+
report.commit = await git(target, ["rev-parse", "HEAD"]);
|
|
2082
|
+
})
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
var fileProgram = String.raw`
|
|
2086
|
+
const fs = require('node:fs/promises'); const path = require('node:path');
|
|
2087
|
+
(async () => {
|
|
2088
|
+
const [root, op, name, content] = process.argv.slice(1);
|
|
2089
|
+
const base = await fs.realpath(root);
|
|
2090
|
+
const target = path.resolve(base, name || '.');
|
|
2091
|
+
const rel = path.relative(base, target);
|
|
2092
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) throw Error('scope');
|
|
2093
|
+
let cursor = base;
|
|
2094
|
+
for (const part of rel.split(path.sep).filter(Boolean)) {
|
|
2095
|
+
cursor = path.join(cursor, part);
|
|
2096
|
+
try { if ((await fs.lstat(cursor)).isSymbolicLink()) throw Error('link'); }
|
|
2097
|
+
catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
2098
|
+
}
|
|
2099
|
+
if (op === 'mkdir') await fs.mkdir(target, { recursive: true });
|
|
2100
|
+
else if (op === 'exists') process.stdout.write(String(await fs.stat(target).then(() => true, e => { if(e.code === 'ENOENT') return false; throw e; })));
|
|
2101
|
+
else if (op === 'write') { await fs.mkdir(path.dirname(target), { recursive: true }); await fs.writeFile(target, Buffer.from(content, 'base64'), { flag: 'wx', mode: 0o600 }); }
|
|
2102
|
+
else if (op === 'read') process.stdout.write((await fs.readFile(target)).toString('base64'));
|
|
2103
|
+
else if (op === 'copy') {
|
|
2104
|
+
const destination = path.resolve(base, content);
|
|
2105
|
+
const destRel = path.relative(base, destination);
|
|
2106
|
+
if(destRel.startsWith('..') || path.isAbsolute(destRel)) throw Error('scope');
|
|
2107
|
+
let destCursor = base;
|
|
2108
|
+
for (const part of destRel.split(path.sep).filter(Boolean)) {
|
|
2109
|
+
destCursor = path.join(destCursor, part);
|
|
2110
|
+
try { if ((await fs.lstat(destCursor)).isSymbolicLink()) throw Error('link'); }
|
|
2111
|
+
catch(e) { if(e.code !== 'ENOENT') throw e; }
|
|
2112
|
+
}
|
|
2113
|
+
async function copy(src, dst) {
|
|
2114
|
+
try { if ((await fs.lstat(dst)).isSymbolicLink()) throw Error('link'); }
|
|
2115
|
+
catch(e) { if(e.code !== 'ENOENT') throw e; }
|
|
2116
|
+
const stat = await fs.lstat(src); if(stat.isSymbolicLink()) throw Error('link');
|
|
2117
|
+
if(stat.isDirectory()) { await fs.mkdir(dst, { recursive: true }); for(const item of await fs.readdir(src)) await copy(path.join(src,item),path.join(dst,item)); }
|
|
2118
|
+
else if(stat.isFile()) await fs.copyFile(src,dst,require('node:fs').constants.COPYFILE_EXCL);
|
|
2119
|
+
else throw Error('special file');
|
|
2120
|
+
}
|
|
2121
|
+
await copy(target,destination);
|
|
2122
|
+
}
|
|
2123
|
+
else throw Error('operation');
|
|
2124
|
+
})().catch(() => { process.exitCode = 1 });`;
|
|
2125
|
+
async function selectEngine(preference) {
|
|
2126
|
+
for (const engine of preference === "auto" ? ["podman", "docker"] : [preference]) {
|
|
2127
|
+
try {
|
|
2128
|
+
await execute(engine, ["info"], void 0, void 0, 15e3);
|
|
2129
|
+
return engine;
|
|
2130
|
+
} catch {
|
|
2131
|
+
if (preference !== "auto")
|
|
2132
|
+
throw new ACPFailure("runtime", { engine });
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
throw new ACPFailure("runtime");
|
|
2136
|
+
}
|
|
2137
|
+
async function openRuntime(workspace) {
|
|
2138
|
+
const environment = {};
|
|
2139
|
+
for (const [target, source] of Object.entries(workspace.environmentRefs)) {
|
|
2140
|
+
if (!process2.env[source])
|
|
2141
|
+
throw new ACPFailure("invalid", { environment: source });
|
|
2142
|
+
environment[target] = process2.env[source];
|
|
2143
|
+
}
|
|
2144
|
+
let root;
|
|
2145
|
+
let container;
|
|
2146
|
+
let engine;
|
|
2147
|
+
let closed = false;
|
|
2148
|
+
if (workspace.storage.type === "directory") {
|
|
2149
|
+
await mkdir(workspace.storage.path, { recursive: true });
|
|
2150
|
+
root = await realpath(resolve(workspace.storage.path));
|
|
2151
|
+
} else {
|
|
2152
|
+
engine = await selectEngine(workspace.storage.engine);
|
|
2153
|
+
root = "/workspace";
|
|
2154
|
+
container = await execute(engine, [
|
|
2155
|
+
"create",
|
|
2156
|
+
// 远程 Podman 会从服务端继承代理,绕过最小环境白名单;只允许显式 environmentRefs 注入。
|
|
2157
|
+
...engine === "podman" ? ["--http-proxy=false"] : [],
|
|
2158
|
+
"--init",
|
|
2159
|
+
"--mount",
|
|
2160
|
+
`type=volume,src=${workspace.storage.name},dst=/workspace`,
|
|
2161
|
+
"--mount",
|
|
2162
|
+
`type=volume,src=${workspace.storage.name}-state,dst=/state`,
|
|
2163
|
+
"--workdir",
|
|
2164
|
+
root,
|
|
2165
|
+
"--entrypoint",
|
|
2166
|
+
"node",
|
|
2167
|
+
workspace.storage.image,
|
|
2168
|
+
"-e",
|
|
2169
|
+
"setInterval(() => {}, 2147483647)"
|
|
2170
|
+
]);
|
|
2171
|
+
if (!/^[a-f0-9]{12,64}$/.test(container))
|
|
2172
|
+
throw new ACPFailure("runtime");
|
|
2173
|
+
try {
|
|
2174
|
+
await execute(engine, ["start", container]);
|
|
2175
|
+
} catch (error) {
|
|
2176
|
+
await execute(engine, ["rm", "-f", container]);
|
|
2177
|
+
throw error;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
const ensure = () => {
|
|
2181
|
+
if (closed)
|
|
2182
|
+
throw new ACPFailure("closed");
|
|
2183
|
+
};
|
|
2184
|
+
const path = (relative2) => workspace.storage.type === "volume" ? `${root}/${relative2}` : resolve(root, relative2);
|
|
2185
|
+
function transport(command, args, cwd, env = {}) {
|
|
2186
|
+
ensure();
|
|
2187
|
+
const values = { ...environment, ...env };
|
|
2188
|
+
return engine && container ? {
|
|
2189
|
+
type: "stdio",
|
|
2190
|
+
command: engine,
|
|
2191
|
+
args: [
|
|
2192
|
+
"exec",
|
|
2193
|
+
"-i",
|
|
2194
|
+
"--workdir",
|
|
2195
|
+
cwd,
|
|
2196
|
+
...Object.keys(values).flatMap((key) => [
|
|
2197
|
+
"--env",
|
|
2198
|
+
Object.hasOwn(engineEnvironment(), key) ? `${key}=${values[key]}` : key
|
|
2199
|
+
]),
|
|
2200
|
+
container,
|
|
2201
|
+
command,
|
|
2202
|
+
...args
|
|
2203
|
+
],
|
|
2204
|
+
env: { ...values, ...engineEnvironment() }
|
|
2205
|
+
} : { type: "stdio", command, args, cwd, env: values };
|
|
2206
|
+
}
|
|
2207
|
+
async function run(command, args, cwd = root, env) {
|
|
2208
|
+
const launch = transport(command, args, cwd, env);
|
|
2209
|
+
if (launch.type !== "stdio")
|
|
2210
|
+
throw new ACPFailure("runtime");
|
|
2211
|
+
return execute(launch.command, launch.args ?? [], launch.cwd, launch.env);
|
|
2212
|
+
}
|
|
2213
|
+
return {
|
|
2214
|
+
root,
|
|
2215
|
+
path,
|
|
2216
|
+
transport,
|
|
2217
|
+
run,
|
|
2218
|
+
resource: container ? { engine, container } : void 0,
|
|
2219
|
+
/**
|
|
2220
|
+
* 计算受管理的会话状态目录。
|
|
2221
|
+
* @param sessionId - 已校验的会话 ID。
|
|
2222
|
+
* @returns 状态目录绝对路径;不创建目录。
|
|
2223
|
+
* @example
|
|
2224
|
+
* ```ts
|
|
2225
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2226
|
+
* const state = runtime.state('task-1')
|
|
2227
|
+
* ```
|
|
2228
|
+
*/
|
|
2229
|
+
state: (sessionId) => workspace.storage.type === "volume" ? `/state/${sessionId}` : path(`.state/${sessionId}`),
|
|
2230
|
+
/**
|
|
2231
|
+
* 执行受工作区约束的文件操作,拒绝链接和路径逃逸。
|
|
2232
|
+
* @param op - mkdir/exists/write/read/copy 操作。
|
|
2233
|
+
* @param name - 工作区内相对路径或已校验的内部绝对路径。
|
|
2234
|
+
* @param content - write 时为 Base64,copy 时为目标路径,其余为空。
|
|
2235
|
+
* @returns 文本结果:exists 为 true/false,read 为 Base64,其余为空。
|
|
2236
|
+
* @example
|
|
2237
|
+
* ```ts
|
|
2238
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2239
|
+
* await runtime.file('mkdir', 'main/files')
|
|
2240
|
+
* ```
|
|
2241
|
+
*/
|
|
2242
|
+
async file(op, name, content = "") {
|
|
2243
|
+
return run(
|
|
2244
|
+
workspace.storage.type === "volume" ? "node" : process2.execPath,
|
|
2245
|
+
["-e", fileProgram, root, op, name, content]
|
|
2246
|
+
);
|
|
2247
|
+
},
|
|
2248
|
+
/**
|
|
2249
|
+
* 向已创建的独立状态目录写入配置,拒绝符号链接。
|
|
2250
|
+
* @param sessionId - 已校验的会话 ID。
|
|
2251
|
+
* @param name - 无目录分隔符的配置文件名。
|
|
2252
|
+
* @param content - 原生配置文本,可能含凭据,不应记录日志。
|
|
2253
|
+
* @returns 完成写入的 Promise。
|
|
2254
|
+
* @example
|
|
2255
|
+
* ```ts
|
|
2256
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2257
|
+
* await runtime.writeState('task-1', 'config.json', '{}')
|
|
2258
|
+
* ```
|
|
2259
|
+
*/
|
|
2260
|
+
async writeState(sessionId, name, content) {
|
|
2261
|
+
if (!/^[\w-]+\.[a-z0-9]+$/i.test(name))
|
|
2262
|
+
throw new ACPFailure("invalid");
|
|
2263
|
+
const directory = workspace.storage.type === "volume" ? `/state/${sessionId}` : path(`.state/${sessionId}`);
|
|
2264
|
+
await run(
|
|
2265
|
+
workspace.storage.type === "volume" ? "node" : process2.execPath,
|
|
2266
|
+
[
|
|
2267
|
+
"-e",
|
|
2268
|
+
'const f=require("node:fs"),p=require("node:path");const d=process.argv[1];let c=p.parse(d).root;for(const part of d.slice(c.length).split(p.sep).filter(Boolean)){c=p.join(c,part);if(f.lstatSync(c).isSymbolicLink())process.exit(1)}const t=p.join(d,process.argv[2]);try{if(f.lstatSync(t).isSymbolicLink())process.exit(1)}catch(e){if(e.code!=="ENOENT")throw e}f.writeFileSync(t,process.argv[3],{mode:384})',
|
|
2269
|
+
directory,
|
|
2270
|
+
name,
|
|
2271
|
+
content
|
|
2272
|
+
]
|
|
2273
|
+
);
|
|
2274
|
+
},
|
|
2275
|
+
/**
|
|
2276
|
+
* 创建私有状态目录,拒绝祖先目录中的符号链接。
|
|
2277
|
+
* @param sessionId - 已校验的会话 ID。
|
|
2278
|
+
* @returns 已创建的状态目录绝对路径。
|
|
2279
|
+
* @example
|
|
2280
|
+
* ```ts
|
|
2281
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2282
|
+
* const directory = await runtime.stateDirectory('task-1')
|
|
2283
|
+
* ```
|
|
2284
|
+
*/
|
|
2285
|
+
async stateDirectory(sessionId) {
|
|
2286
|
+
const target = workspace.storage.type === "volume" ? `/state/${sessionId}` : path(`.state/${sessionId}`);
|
|
2287
|
+
await run(
|
|
2288
|
+
workspace.storage.type === "volume" ? "node" : process2.execPath,
|
|
2289
|
+
[
|
|
2290
|
+
"-e",
|
|
2291
|
+
'const f=require("node:fs"),p=require("node:path"),d=process.argv[1];let c=p.parse(d).root;for(const part of d.slice(c.length).split(p.sep).filter(Boolean)){c=p.join(c,part);try{if(f.lstatSync(c).isSymbolicLink())process.exit(1)}catch(e){if(e.code!=="ENOENT")throw e;f.mkdirSync(c,{mode:448})}}',
|
|
2292
|
+
target
|
|
2293
|
+
]
|
|
2294
|
+
);
|
|
2295
|
+
return target;
|
|
2296
|
+
},
|
|
2297
|
+
/**
|
|
2298
|
+
* 关闭本次拥有的容器;目录和 volume 保留。
|
|
2299
|
+
* @returns 清理完成的 Promise;失败抛错,不能视为资源已释放。
|
|
2300
|
+
* @example
|
|
2301
|
+
* ```ts
|
|
2302
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2303
|
+
* await runtime.close()
|
|
2304
|
+
* ```
|
|
2305
|
+
*/
|
|
2306
|
+
async close() {
|
|
2307
|
+
if (closed)
|
|
2308
|
+
return;
|
|
2309
|
+
if (engine && container) {
|
|
2310
|
+
try {
|
|
2311
|
+
await execute(engine, ["rm", "--force", container]);
|
|
2312
|
+
} catch {
|
|
2313
|
+
throw new ACPFailure("cleanup", { engine, container });
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
closed = true;
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// src/acp/ai-acp-functions.ts
|
|
2322
|
+
function createACPOperations(provider, config, extensions = {}, modelConfig = async () => config, mcpLookup) {
|
|
2323
|
+
const workspaces = provider.createRelStore(
|
|
2324
|
+
"hai_ai_acp_workspaces"
|
|
2325
|
+
);
|
|
2326
|
+
const agents = provider.createRelStore("hai_ai_acp_agents");
|
|
2327
|
+
const sessions = provider.createRelStore(
|
|
2328
|
+
"hai_ai_acp_sessions"
|
|
2329
|
+
);
|
|
2330
|
+
const locks = provider.createRelStore(
|
|
2331
|
+
"hai_ai_acp_locks"
|
|
2332
|
+
);
|
|
2333
|
+
const adapters = { ...nativeAdapters, ...extensions };
|
|
2334
|
+
const live = /* @__PURE__ */ new Map();
|
|
2335
|
+
const connections = /* @__PURE__ */ new Set();
|
|
2336
|
+
const limits = ACPConfigSchema.parse(config.acp ?? {});
|
|
2337
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2338
|
+
const stopping = /* @__PURE__ */ new Map();
|
|
2339
|
+
let closed = false;
|
|
2340
|
+
const agentRegistry = createResourceRegistry(
|
|
2341
|
+
agents,
|
|
2342
|
+
ACPAgentSchema,
|
|
2343
|
+
config.acp?.agents ?? [],
|
|
2344
|
+
() => !closed
|
|
2345
|
+
);
|
|
2346
|
+
const workspaceRegistry = createResourceRegistry(
|
|
2347
|
+
workspaces,
|
|
2348
|
+
ACPWorkspaceSchema,
|
|
2349
|
+
config.acp?.workspaces ?? [],
|
|
2350
|
+
() => !closed
|
|
2351
|
+
);
|
|
2352
|
+
function ensure() {
|
|
2353
|
+
if (closed)
|
|
2354
|
+
throw new ACPFailure("closed");
|
|
2355
|
+
}
|
|
2356
|
+
async function protect(operation) {
|
|
2357
|
+
const task = (async () => {
|
|
2358
|
+
try {
|
|
2359
|
+
ensure();
|
|
2360
|
+
return ok(await operation());
|
|
2361
|
+
} catch (error) {
|
|
2362
|
+
return acpError(error);
|
|
2363
|
+
}
|
|
2364
|
+
})();
|
|
2365
|
+
pending.add(task);
|
|
2366
|
+
try {
|
|
2367
|
+
return await task;
|
|
2368
|
+
} finally {
|
|
2369
|
+
pending.delete(task);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
async function modelSignature(agent) {
|
|
2373
|
+
return createHash("sha256").update(
|
|
2374
|
+
JSON.stringify(
|
|
2375
|
+
resolveAgentModels(agent, await modelConfig()).map(
|
|
2376
|
+
({ id, config: model }) => ({
|
|
2377
|
+
id,
|
|
2378
|
+
model: model.model,
|
|
2379
|
+
protocol: model.protocol,
|
|
2380
|
+
baseUrl: model.baseUrl,
|
|
2381
|
+
capabilities: model.capabilities
|
|
2382
|
+
})
|
|
2383
|
+
)
|
|
2384
|
+
)
|
|
2385
|
+
).digest("hex");
|
|
2386
|
+
}
|
|
2387
|
+
async function protectResult(operation) {
|
|
2388
|
+
const result = await protect(operation);
|
|
2389
|
+
return result.success ? result.data : result;
|
|
2390
|
+
}
|
|
2391
|
+
async function getAgent(id) {
|
|
2392
|
+
const entry = await agentRegistry.read(id);
|
|
2393
|
+
if (!entry)
|
|
2394
|
+
throw new ACPFailure("missing", { id });
|
|
2395
|
+
return entry.value;
|
|
2396
|
+
}
|
|
2397
|
+
async function getWorkspace(id) {
|
|
2398
|
+
const entry = await workspaceRegistry.read(id);
|
|
2399
|
+
if (!entry)
|
|
2400
|
+
throw new ACPFailure("missing", { id });
|
|
2401
|
+
const workspace = await normalizeWorkspace(entry.value);
|
|
2402
|
+
await checkWorkspaceOverlap(workspace);
|
|
2403
|
+
return workspace;
|
|
2404
|
+
}
|
|
2405
|
+
const workspaceKey = (workspace) => workspace.storage.type === "directory" ? `directory:${process2.platform === "win32" ? workspace.storage.path.toLowerCase() : workspace.storage.path}` : `volume:${workspace.storage.engine}:${workspace.storage.name}`;
|
|
2406
|
+
async function normalizeWorkspace(workspace) {
|
|
2407
|
+
if (workspace.storage.type === "directory") {
|
|
2408
|
+
await mkdir(workspace.storage.path, { recursive: true });
|
|
2409
|
+
workspace.storage.path = await realpath(resolve(workspace.storage.path));
|
|
2410
|
+
} else {
|
|
2411
|
+
workspace.storage.engine = await selectEngine(workspace.storage.engine);
|
|
2412
|
+
}
|
|
2413
|
+
return workspace;
|
|
2414
|
+
}
|
|
2415
|
+
async function canonicalDirectory(path) {
|
|
2416
|
+
const target = resolve(path);
|
|
2417
|
+
try {
|
|
2418
|
+
return await realpath(target);
|
|
2419
|
+
} catch (error) {
|
|
2420
|
+
if (error.code !== "ENOENT" || dirname(target) === target) {
|
|
2421
|
+
throw error;
|
|
2422
|
+
}
|
|
2423
|
+
return resolve(
|
|
2424
|
+
await canonicalDirectory(dirname(target)),
|
|
2425
|
+
basename(target)
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
async function checkWorkspaceOverlap(workspace) {
|
|
2430
|
+
for (const entry of await workspaceRegistry.values()) {
|
|
2431
|
+
if (entry.id === workspace.id)
|
|
2432
|
+
continue;
|
|
2433
|
+
if (entry.storage.type === "directory" && workspace.storage.type === "directory") {
|
|
2434
|
+
const existing = await canonicalDirectory(entry.storage.path);
|
|
2435
|
+
const child = relative(existing, workspace.storage.path);
|
|
2436
|
+
const parent = relative(workspace.storage.path, existing);
|
|
2437
|
+
if (!child.startsWith("..") && !isAbsolute(child) || !parent.startsWith("..") && !isAbsolute(parent)) {
|
|
2438
|
+
throw new ACPFailure("conflict");
|
|
2439
|
+
}
|
|
2440
|
+
} else if (entry.storage.type === "volume" && workspace.storage.type === "volume" && entry.storage.name === workspace.storage.name && (entry.storage.engine === "auto" || entry.storage.engine === workspace.storage.engine)) {
|
|
2441
|
+
throw new ACPFailure("conflict");
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
async function get(store, id) {
|
|
2446
|
+
sessionIdentitySchema.parse(id);
|
|
2447
|
+
const data = await store.get(id);
|
|
2448
|
+
if (!data)
|
|
2449
|
+
throw new ACPFailure("missing", { id });
|
|
2450
|
+
return data;
|
|
2451
|
+
}
|
|
2452
|
+
async function acquire(key) {
|
|
2453
|
+
const previous = await locks.readVersioned(key);
|
|
2454
|
+
if (previous?.data.owner)
|
|
2455
|
+
throw new ACPFailure("busy");
|
|
2456
|
+
const owner = randomUUID();
|
|
2457
|
+
const version = await locks.compareAndSwap(key, {
|
|
2458
|
+
expectedVersion: previous?.version,
|
|
2459
|
+
data: { owner }
|
|
2460
|
+
});
|
|
2461
|
+
if (!version)
|
|
2462
|
+
throw new ACPFailure("busy");
|
|
2463
|
+
return async () => {
|
|
2464
|
+
if (!await locks.compareAndSwap(key, {
|
|
2465
|
+
expectedVersion: version,
|
|
2466
|
+
data: { owner: null }
|
|
2467
|
+
})) {
|
|
2468
|
+
throw new ACPFailure("cleanup");
|
|
2469
|
+
}
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
async function withDefinitions(operation) {
|
|
2473
|
+
const release = await acquire("definition-registry");
|
|
2474
|
+
try {
|
|
2475
|
+
return await operation();
|
|
2476
|
+
} finally {
|
|
2477
|
+
await release();
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
async function workspaceOperation(workspace, action) {
|
|
2481
|
+
const release = await acquire(`${workspaceKey(workspace)}:main`);
|
|
2482
|
+
let runtime;
|
|
2483
|
+
try {
|
|
2484
|
+
runtime = await openRuntime(workspace);
|
|
2485
|
+
return await action(runtime);
|
|
2486
|
+
} finally {
|
|
2487
|
+
await runtime?.close();
|
|
2488
|
+
await release();
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
async function unique(store, id, data) {
|
|
2492
|
+
if (!await store.compareAndSwap(id, { expectedVersion: void 0, data }))
|
|
2493
|
+
throw new ACPFailure("conflict", { id });
|
|
2494
|
+
return data;
|
|
2495
|
+
}
|
|
2496
|
+
async function validateAgent(agent) {
|
|
2497
|
+
if (agent.mode === "custom-service")
|
|
2498
|
+
return;
|
|
2499
|
+
if (!adapters[agent.adapter])
|
|
2500
|
+
throw new ACPFailure("unsupported", { adapter: agent.adapter });
|
|
2501
|
+
resolveAgentModels(agent, await modelConfig());
|
|
2502
|
+
}
|
|
2503
|
+
async function launchAgent(agent, runtime, info) {
|
|
2504
|
+
if (agent.mode === "custom-service") {
|
|
2505
|
+
return {
|
|
2506
|
+
type: agent.transport,
|
|
2507
|
+
url: agent.url,
|
|
2508
|
+
headers: resolveEnvironment(agent.headerEnv)
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
if (!runtime)
|
|
2512
|
+
throw new ACPFailure("missing");
|
|
2513
|
+
const statePath = await runtime.stateDirectory(info.id);
|
|
2514
|
+
const native = await adapters[agent.adapter].configure({
|
|
2515
|
+
agent,
|
|
2516
|
+
models: resolveAgentModels(agent, await modelConfig()),
|
|
2517
|
+
modelId: info.modelId,
|
|
2518
|
+
statePath
|
|
2519
|
+
});
|
|
2520
|
+
for (const [name, content] of Object.entries(native.files ?? {}))
|
|
2521
|
+
await runtime.writeState(info.id, name, content);
|
|
2522
|
+
return runtime.transport(native.command, native.args, info.cwd, {
|
|
2523
|
+
HOME: statePath,
|
|
2524
|
+
USERPROFILE: statePath,
|
|
2525
|
+
...resolveEnvironment(agent.environmentRefs),
|
|
2526
|
+
...native.env
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
async function stop(id) {
|
|
2530
|
+
const existing = stopping.get(id);
|
|
2531
|
+
if (existing)
|
|
2532
|
+
return existing;
|
|
2533
|
+
const task = stopOne(id);
|
|
2534
|
+
stopping.set(id, task);
|
|
2535
|
+
try {
|
|
2536
|
+
await task;
|
|
2537
|
+
} finally {
|
|
2538
|
+
stopping.delete(id);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
async function stopOne(id) {
|
|
2542
|
+
const current = live.get(id);
|
|
2543
|
+
if (!current)
|
|
2544
|
+
return;
|
|
2545
|
+
await current.connection.cancel();
|
|
2546
|
+
try {
|
|
2547
|
+
await current.connection.closeSession();
|
|
2548
|
+
} catch {
|
|
2549
|
+
} finally {
|
|
2550
|
+
await current.connection.close();
|
|
2551
|
+
await current.runtime?.close();
|
|
2552
|
+
}
|
|
2553
|
+
current.info.status = "closed";
|
|
2554
|
+
current.info.resource = void 0;
|
|
2555
|
+
await sessions.save(id, current.info);
|
|
2556
|
+
await current.release();
|
|
2557
|
+
live.delete(id);
|
|
2558
|
+
}
|
|
2559
|
+
async function start(info, options, resume) {
|
|
2560
|
+
const agent = info.agent;
|
|
2561
|
+
const releaseSession = await acquire(`session:${info.id}`);
|
|
2562
|
+
let releaseDirectory;
|
|
2563
|
+
let runtime;
|
|
2564
|
+
let connection;
|
|
2565
|
+
let recordExists = true;
|
|
2566
|
+
try {
|
|
2567
|
+
const workspace = info.workspaceId ? await getWorkspace(info.workspaceId) : void 0;
|
|
2568
|
+
if (!agent.enabled)
|
|
2569
|
+
throw new ACPFailure("unsupported");
|
|
2570
|
+
if (resume && !await sessions.get(info.id)) {
|
|
2571
|
+
recordExists = false;
|
|
2572
|
+
throw new ACPFailure("missing");
|
|
2573
|
+
}
|
|
2574
|
+
if (workspace) {
|
|
2575
|
+
if (agent.mode === "custom-service")
|
|
2576
|
+
throw new ACPFailure("unsupported");
|
|
2577
|
+
if (agent.mode === "embedded" && workspace.storage.type !== "directory" || agent.mode === "builtin-service" && workspace.storage.type !== "volume") {
|
|
2578
|
+
throw new ACPFailure("unsupported");
|
|
2579
|
+
}
|
|
2580
|
+
if (agent.mode === "builtin-service" && workspace.storage.type === "volume" && (agent.image !== workspace.storage.image || agent.engine !== "auto" && agent.engine !== workspace.storage.engine)) {
|
|
2581
|
+
throw new ACPFailure("conflict");
|
|
2582
|
+
}
|
|
2583
|
+
if (!info.newWorktree)
|
|
2584
|
+
releaseDirectory = await acquire(`${workspaceKey(workspace)}:main`);
|
|
2585
|
+
if (!resume) {
|
|
2586
|
+
const releasePrepare = info.newWorktree ? await acquire(`${workspaceKey(workspace)}:main`) : void 0;
|
|
2587
|
+
try {
|
|
2588
|
+
runtime = await openRuntime(workspace);
|
|
2589
|
+
info.resource = runtime.resource;
|
|
2590
|
+
await sessions.save(info.id, info);
|
|
2591
|
+
const git = createGitOperations(workspace, runtime);
|
|
2592
|
+
await git.prepare();
|
|
2593
|
+
info.cwd = info.newWorktree ? await git.createWorktree(info.id, info.branch) : runtime.path("main");
|
|
2594
|
+
} finally {
|
|
2595
|
+
await releasePrepare?.();
|
|
2596
|
+
}
|
|
2597
|
+
} else {
|
|
2598
|
+
runtime = await openRuntime(workspace);
|
|
2599
|
+
}
|
|
2600
|
+
} else if (agent.mode !== "custom-service" || !info.cwd || info.newWorktree) {
|
|
2601
|
+
throw new ACPFailure("invalid");
|
|
2602
|
+
}
|
|
2603
|
+
ensure();
|
|
2604
|
+
const transport = await launchAgent(agent, runtime, info);
|
|
2605
|
+
connection = await connectACP(transport, options, limits);
|
|
2606
|
+
if (info.mcpServerIds?.length && !mcpLookup)
|
|
2607
|
+
throw new ACPFailure("unsupported");
|
|
2608
|
+
const mcpServers = [
|
|
2609
|
+
...info.mcpServers,
|
|
2610
|
+
...info.mcpServerIds?.length && mcpLookup ? await mcpLookup(info.mcpServerIds) : []
|
|
2611
|
+
];
|
|
2612
|
+
if (resume) {
|
|
2613
|
+
if (!info.nativeId)
|
|
2614
|
+
throw new ACPFailure("unsupported");
|
|
2615
|
+
await connection.resumeSession(info.nativeId, info.cwd, mcpServers);
|
|
2616
|
+
} else {
|
|
2617
|
+
info.nativeId = (await connection.createSession(info.cwd, mcpServers)).sessionId;
|
|
2618
|
+
}
|
|
2619
|
+
ensure();
|
|
2620
|
+
info.status = "active";
|
|
2621
|
+
info.resource = runtime?.resource;
|
|
2622
|
+
await sessions.save(info.id, info);
|
|
2623
|
+
const value = {
|
|
2624
|
+
info,
|
|
2625
|
+
connection,
|
|
2626
|
+
runtime,
|
|
2627
|
+
release: async () => {
|
|
2628
|
+
await releaseDirectory?.();
|
|
2629
|
+
await releaseSession();
|
|
2630
|
+
}
|
|
2631
|
+
};
|
|
2632
|
+
live.set(info.id, value);
|
|
2633
|
+
return handle(info.id, value);
|
|
2634
|
+
} catch (error) {
|
|
2635
|
+
await connection?.close();
|
|
2636
|
+
await runtime?.close();
|
|
2637
|
+
info.status = "failed";
|
|
2638
|
+
info.resource = void 0;
|
|
2639
|
+
if (recordExists)
|
|
2640
|
+
await sessions.save(info.id, info);
|
|
2641
|
+
await releaseDirectory?.();
|
|
2642
|
+
await releaseSession();
|
|
2643
|
+
throw error;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
function handle(id, expected) {
|
|
2647
|
+
function current() {
|
|
2648
|
+
ensure();
|
|
2649
|
+
const value = live.get(id);
|
|
2650
|
+
if (!value || expected && value !== expected)
|
|
2651
|
+
throw new ACPFailure("closed");
|
|
2652
|
+
return value;
|
|
2653
|
+
}
|
|
2654
|
+
async function gitOperation(publish) {
|
|
2655
|
+
const request = publish ? ACPPublishSchema.parse(publish) : void 0;
|
|
2656
|
+
const info = await get(sessions, id);
|
|
2657
|
+
if (!info.workspaceId || request && !info.newWorktree)
|
|
2658
|
+
throw new ACPFailure("unsupported");
|
|
2659
|
+
const workspace = await getWorkspace(info.workspaceId);
|
|
2660
|
+
await stop(id);
|
|
2661
|
+
const release = await acquire(`session:${id}`);
|
|
2662
|
+
try {
|
|
2663
|
+
return await workspaceOperation(workspace, (runtime) => request ? createGitOperations(workspace, runtime).publish(
|
|
2664
|
+
id,
|
|
2665
|
+
info.branch,
|
|
2666
|
+
request
|
|
2667
|
+
) : createGitOperations(workspace, runtime).updateMain());
|
|
2668
|
+
} finally {
|
|
2669
|
+
await release();
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
return {
|
|
2673
|
+
id,
|
|
2674
|
+
/**
|
|
2675
|
+
* 读取持久会话信息,包括状态、原生 ID 和当前容器。
|
|
2676
|
+
* @returns 包含 ACPSessionInfo 的 HaiResult;记录不存在时返回失败。
|
|
2677
|
+
* @example
|
|
2678
|
+
* ```ts
|
|
2679
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2680
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2681
|
+
* const result = await session.info()
|
|
2682
|
+
* ```
|
|
2683
|
+
*/
|
|
2684
|
+
info: () => protect(() => get(sessions, id)),
|
|
2685
|
+
/**
|
|
2686
|
+
* 发送一轮内容,等待最终停止原因、最终消息文本和完整事件;同一会话不允许并发轮次。
|
|
2687
|
+
* @param content - 原生 ACP 内容块,不允许同一会话并发执行。
|
|
2688
|
+
* @param signal - 本轮取消信号;取消超时会关闭连接。
|
|
2689
|
+
* @returns 最终文本、停止原因和事件的 HaiResult。
|
|
2690
|
+
* @example
|
|
2691
|
+
* ```ts
|
|
2692
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2693
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2694
|
+
* const result = await session.prompt([{ type: 'text', text: '检查工程' }])
|
|
2695
|
+
* ```
|
|
2696
|
+
*/
|
|
2697
|
+
prompt: (content, signal) => protect(() => current().connection.prompt(content, signal)),
|
|
2698
|
+
/**
|
|
2699
|
+
* 仅接受正常结束的一份 JSON(允许完整 JSON 代码围栏),再按 schema 校验;前后夹杂文本视为失败。
|
|
2700
|
+
* @param content - 要求返回结构化 JSON 的原生内容块。
|
|
2701
|
+
* @param schema - 最终 JSON 的 Zod 校验器。
|
|
2702
|
+
* @param signal - 可选的本轮取消信号。
|
|
2703
|
+
* @returns 经过 schema 校验的数据;输出不合法时返回 ACP_OUTPUT_INVALID。
|
|
2704
|
+
* @example
|
|
2705
|
+
* ```ts
|
|
2706
|
+
* import { z } from 'zod'
|
|
2707
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2708
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2709
|
+
* const result = await session.promptStructured([{ type: 'text', text: '返回 JSON' }], z.object({ ok: z.boolean() }))
|
|
2710
|
+
* ```
|
|
2711
|
+
*/
|
|
2712
|
+
promptStructured: (content, schema, signal) => protect(async () => {
|
|
2713
|
+
const response = await current().connection.prompt(content, signal);
|
|
2714
|
+
if (response.stopReason !== "end_turn")
|
|
2715
|
+
throw new ACPFailure("output");
|
|
2716
|
+
let text = response.text.trim();
|
|
2717
|
+
const fenced = /^```(?:json)?[ \t]*\n([\s\S]*?)\n```$/.exec(text);
|
|
2718
|
+
if (fenced)
|
|
2719
|
+
text = fenced[1];
|
|
2720
|
+
try {
|
|
2721
|
+
return schema.parse(JSON.parse(text));
|
|
2722
|
+
} catch {
|
|
2723
|
+
throw new ACPFailure("output");
|
|
2724
|
+
}
|
|
2725
|
+
}),
|
|
2726
|
+
/**
|
|
2727
|
+
* 迭代本轮 ACP 事件;中途退出迭代会取消本轮,失败会抛错,调用方需 try/finally 清理。
|
|
2728
|
+
* @param content - 本轮原生内容块。
|
|
2729
|
+
* @param signal - 可选取消信号;结束迭代同样会取消本轮。
|
|
2730
|
+
* @returns 异步事件迭代器;流式失败抛出异常,需在调用方捕获。
|
|
2731
|
+
* @throws 连接关闭、取消超时、输出超限或原生请求失败。
|
|
2732
|
+
* @example
|
|
2733
|
+
* ```ts
|
|
2734
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2735
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2736
|
+
* for await (const event of session.stream([{ type: 'text', text: '检查工程' }])) { const update = event.update }
|
|
2737
|
+
* ```
|
|
2738
|
+
*/
|
|
2739
|
+
async *stream(content, signal) {
|
|
2740
|
+
const value = current();
|
|
2741
|
+
const controller = new AbortController();
|
|
2742
|
+
const queue = [];
|
|
2743
|
+
let wake;
|
|
2744
|
+
const completion = { done: false };
|
|
2745
|
+
let error;
|
|
2746
|
+
const unsubscribe = value.connection.subscribe((event) => {
|
|
2747
|
+
if (queue.length >= limits.maxPendingEvents)
|
|
2748
|
+
throw new ACPFailure("output");
|
|
2749
|
+
queue.push(event);
|
|
2750
|
+
wake?.();
|
|
2751
|
+
});
|
|
2752
|
+
const task = value.connection.prompt(
|
|
2753
|
+
content,
|
|
2754
|
+
signal ? AbortSignal.any([signal, controller.signal]) : controller.signal
|
|
2755
|
+
).catch((reason) => {
|
|
2756
|
+
error = reason;
|
|
2757
|
+
}).finally(() => {
|
|
2758
|
+
completion.done = true;
|
|
2759
|
+
wake?.();
|
|
2760
|
+
});
|
|
2761
|
+
try {
|
|
2762
|
+
while (!completion.done || queue.length) {
|
|
2763
|
+
if (queue.length) {
|
|
2764
|
+
yield queue.shift();
|
|
2765
|
+
} else {
|
|
2766
|
+
await new Promise((resolve3) => {
|
|
2767
|
+
wake = resolve3;
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
if (error)
|
|
2772
|
+
throw error;
|
|
2773
|
+
} finally {
|
|
2774
|
+
unsubscribe();
|
|
2775
|
+
controller.abort();
|
|
2776
|
+
await task;
|
|
2777
|
+
}
|
|
2778
|
+
},
|
|
2779
|
+
/**
|
|
2780
|
+
* 请求取消正在执行的一轮;超时未停止时关闭所拥有的传输。
|
|
2781
|
+
* @returns 取消完成的 HaiResult;超时或清理失败返回错误。
|
|
2782
|
+
* @example
|
|
2783
|
+
* ```ts
|
|
2784
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2785
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2786
|
+
* const result = await session.cancel()
|
|
2787
|
+
* ```
|
|
2788
|
+
*/
|
|
2789
|
+
cancel: () => protect(() => current().connection.cancel()),
|
|
2790
|
+
/**
|
|
2791
|
+
* 选择智能体公布的原生模式 ID;不兼容时返回协议失败。
|
|
2792
|
+
* @param mode - 握手或事件中公布的原生模式 ID。
|
|
2793
|
+
* @returns 原生模式设置结果的 HaiResult。
|
|
2794
|
+
* @example
|
|
2795
|
+
* ```ts
|
|
2796
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2797
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2798
|
+
* const result = await session.setMode('code')
|
|
2799
|
+
* ```
|
|
2800
|
+
*/
|
|
2801
|
+
setMode: (mode) => protect(() => current().connection.setMode(mode)),
|
|
2802
|
+
/**
|
|
2803
|
+
* 设置智能体公布的原生配置选项;值必须符合该智能体的选项定义。
|
|
2804
|
+
* @param key - 原生配置选项 ID。
|
|
2805
|
+
* @param value - 该选项允许的值。
|
|
2806
|
+
* @returns 原生配置更新结果的 HaiResult。
|
|
2807
|
+
* @example
|
|
2808
|
+
* ```ts
|
|
2809
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2810
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2811
|
+
* const result = await session.setConfigOption('model', 'coding')
|
|
2812
|
+
* ```
|
|
2813
|
+
*/
|
|
2814
|
+
setConfigOption: (key, value) => protect(() => current().connection.setConfigOption(key, value)),
|
|
2815
|
+
/**
|
|
2816
|
+
* 先关闭会话执行环境,再 fetch 并仅快进各工程明确配置的主分支;不修改会话分支。
|
|
2817
|
+
* @returns 各仓更新报告;部分失败时报告位于 error.cause.reports。
|
|
2818
|
+
* @example
|
|
2819
|
+
* ```ts
|
|
2820
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2821
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2822
|
+
* const result = await session.updateMain()
|
|
2823
|
+
* ```
|
|
2824
|
+
*/
|
|
2825
|
+
updateMain: () => protect(() => gitOperation()),
|
|
2826
|
+
/**
|
|
2827
|
+
* 关闭执行环境后逐仓提交并推送;仅支持受管理的独立会话,部分失败保留各仓报告,禁止强推。
|
|
2828
|
+
* @param request - 发布模式、提交说明和作者;仅支持受管理的独立会话。
|
|
2829
|
+
* @returns 各仓提交/推送报告;部分失败时报告位于 error.cause.reports。
|
|
2830
|
+
* @example
|
|
2831
|
+
* ```ts
|
|
2832
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2833
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2834
|
+
* const result = await session.publish({ mode: 'merge-main', message: '完成任务', author: { name: 'Developer', email: 'dev@example.com' } })
|
|
2835
|
+
* ```
|
|
2836
|
+
*/
|
|
2837
|
+
publish: (request) => protect(() => gitOperation(request)),
|
|
2838
|
+
/**
|
|
2839
|
+
* 停止拥有的进程或容器,释放占用;保留工作目录和原生状态,供后续显式恢复。
|
|
2840
|
+
* @returns 关闭完成的 HaiResult;清理失败时不释放仍在占用的资源。
|
|
2841
|
+
* @example
|
|
2842
|
+
* ```ts
|
|
2843
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2844
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2845
|
+
* const result = await session.close()
|
|
2846
|
+
* ```
|
|
2847
|
+
*/
|
|
2848
|
+
close: () => protect(() => stop(id))
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
return {
|
|
2852
|
+
agents: {
|
|
2853
|
+
/**
|
|
2854
|
+
* 校验模型与适配器后新增数据库智能体;配置同名条目只读。
|
|
2855
|
+
* @param input - 完整智能体定义,必须引用已有 Hai 模型 ID。
|
|
2856
|
+
* @returns 带 source/version 的新增条目;配置同 ID 返回只读错误。
|
|
2857
|
+
* @example
|
|
2858
|
+
* ```ts
|
|
2859
|
+
* import { ai } from '@h-ai/ai'
|
|
2860
|
+
* // 前提:ai 已成功初始化。
|
|
2861
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2862
|
+
* const result = await ai.acp.agents.register({ id: 'coder', name: 'Coder', mode: 'embedded', adapter: 'codex', modelIds: ['coding'] })
|
|
2863
|
+
* ```
|
|
2864
|
+
*/
|
|
2865
|
+
register: (input) => protectResult(async () => {
|
|
2866
|
+
const agent = ACPAgentSchema.parse(input);
|
|
2867
|
+
await validateAgent(agent);
|
|
2868
|
+
return agentRegistry.operations.register(agent);
|
|
2869
|
+
}),
|
|
2870
|
+
/**
|
|
2871
|
+
* 按 ID 读取合并清单条目;source 区分配置与数据库,数据库 get 返回 version。
|
|
2872
|
+
* @param id - 智能体 ID。
|
|
2873
|
+
* @returns 合并后的资源条目;未找到时返回资源不存在错误。
|
|
2874
|
+
* @example
|
|
2875
|
+
* ```ts
|
|
2876
|
+
* import { ai } from '@h-ai/ai'
|
|
2877
|
+
* // 前提:ai 已成功初始化。
|
|
2878
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2879
|
+
* const result = await ai.acp.agents.get('coder')
|
|
2880
|
+
* ```
|
|
2881
|
+
*/
|
|
2882
|
+
get: agentRegistry.operations.get,
|
|
2883
|
+
/**
|
|
2884
|
+
* 列出配置与数据库合并后的智能体;同 ID 配置优先。
|
|
2885
|
+
* @returns 去重后的智能体条目列表;列表项不带数据库 version。
|
|
2886
|
+
* @example
|
|
2887
|
+
* ```ts
|
|
2888
|
+
* import { ai } from '@h-ai/ai'
|
|
2889
|
+
* // 前提:ai 已成功初始化。
|
|
2890
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2891
|
+
* const result = await ai.acp.agents.list()
|
|
2892
|
+
* ```
|
|
2893
|
+
*/
|
|
2894
|
+
list: agentRegistry.operations.list,
|
|
2895
|
+
/**
|
|
2896
|
+
* 整体替换数据库智能体;可传入 get 返回的版本防止覆盖并发修改,配置条目不可改。
|
|
2897
|
+
* @param input - 包含原 ID 的完整替换定义。
|
|
2898
|
+
* @param version - get 返回的数据库版本;传入时校验并发修改。
|
|
2899
|
+
* @returns 更新后的资源条目;配置只读、版本冲突均返回失败。
|
|
2900
|
+
* @example
|
|
2901
|
+
* ```ts
|
|
2902
|
+
* import { ai } from '@h-ai/ai'
|
|
2903
|
+
* // 前提:ai 已成功初始化。
|
|
2904
|
+
* // definition 为完整替换定义;entry 为 get 成功返回的条目。
|
|
2905
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2906
|
+
* const result = await ai.acp.agents.update(definition, entry.version)
|
|
2907
|
+
* ```
|
|
2908
|
+
*/
|
|
2909
|
+
update: (input, version) => protectResult(async () => {
|
|
2910
|
+
const agent = ACPAgentSchema.parse(input);
|
|
2911
|
+
await validateAgent(agent);
|
|
2912
|
+
return agentRegistry.operations.update(agent, version);
|
|
2913
|
+
}),
|
|
2914
|
+
/**
|
|
2915
|
+
* 删除无会话引用的数据库智能体定义;不删除镜像或外部服务。
|
|
2916
|
+
* @param id - 数据库智能体 ID。
|
|
2917
|
+
* @param version - 可选的期望版本。
|
|
2918
|
+
* @returns 删除完成的 HaiResult;存在会话引用时返回 ACP_BUSY。
|
|
2919
|
+
* @example
|
|
2920
|
+
* ```ts
|
|
2921
|
+
* import { ai } from '@h-ai/ai'
|
|
2922
|
+
* // 前提:ai 已成功初始化。
|
|
2923
|
+
* // entry 为 get 成功返回的数据库条目。
|
|
2924
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2925
|
+
* const result = await ai.acp.agents.remove('coder', entry.version)
|
|
2926
|
+
* ```
|
|
2927
|
+
*/
|
|
2928
|
+
remove: (id, version) => protectResult(async () => {
|
|
2929
|
+
return withDefinitions(async () => {
|
|
2930
|
+
const existing = await agentRegistry.read(id);
|
|
2931
|
+
if (existing?.source === "config")
|
|
2932
|
+
return agentRegistry.operations.remove(id, version);
|
|
2933
|
+
if ((await sessions.query({ where: { agentId: id } })).length)
|
|
2934
|
+
throw new ACPFailure("busy");
|
|
2935
|
+
return agentRegistry.operations.remove(id, version);
|
|
2936
|
+
});
|
|
2937
|
+
}),
|
|
2938
|
+
/**
|
|
2939
|
+
* 解析模型和原生配置,验证适配器是否兼容;不持久保存模型凭据。
|
|
2940
|
+
* @param id - 已登记智能体 ID。
|
|
2941
|
+
* @param modelId - 可选模型 ID,须在智能体 modelIds 中。
|
|
2942
|
+
* @returns 智能体模式与绑定模型摘要;不返回凭据或原生配置内容。
|
|
2943
|
+
* @example
|
|
2944
|
+
* ```ts
|
|
2945
|
+
* import { ai } from '@h-ai/ai'
|
|
2946
|
+
* // 前提:ai 已成功初始化。
|
|
2947
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2948
|
+
* const result = await ai.acp.agents.initialize('coder')
|
|
2949
|
+
* ```
|
|
2950
|
+
*/
|
|
2951
|
+
initialize: (id, modelId) => protect(async () => {
|
|
2952
|
+
const agent = await getAgent(id);
|
|
2953
|
+
await validateAgent(agent);
|
|
2954
|
+
if (agent.mode === "custom-service")
|
|
2955
|
+
return { agentId: id, mode: agent.mode, modelIds: [] };
|
|
2956
|
+
await adapters[agent.adapter].configure({
|
|
2957
|
+
agent,
|
|
2958
|
+
models: resolveAgentModels(agent, await modelConfig()),
|
|
2959
|
+
modelId: modelId ?? agent.defaultModelId ?? agent.modelIds[0],
|
|
2960
|
+
statePath: "/state/validation"
|
|
2961
|
+
});
|
|
2962
|
+
return { agentId: id, mode: agent.mode, modelIds: agent.modelIds };
|
|
2963
|
+
}),
|
|
2964
|
+
/**
|
|
2965
|
+
* 实际启动并握手后关闭连接,返回智能体能力;托管模式需指定工作区。
|
|
2966
|
+
* @param id - 已登记智能体 ID。
|
|
2967
|
+
* @param options - 回调、能力及工作区;托管模式必须传 workspaceId。
|
|
2968
|
+
* @returns 原生初始化结果(智能体能力、认证方法等)的 HaiResult。
|
|
2969
|
+
* @example
|
|
2970
|
+
* ```ts
|
|
2971
|
+
* import { ai } from '@h-ai/ai'
|
|
2972
|
+
* // 前提:ai 已成功初始化。
|
|
2973
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2974
|
+
* const result = await ai.acp.agents.check('coder', { workspaceId: 'project' })
|
|
2975
|
+
* ```
|
|
2976
|
+
*/
|
|
2977
|
+
check: (id, options = {}) => protect(async () => {
|
|
2978
|
+
const agent = await getAgent(id);
|
|
2979
|
+
await validateAgent(agent);
|
|
2980
|
+
if (agent.mode !== "custom-service") {
|
|
2981
|
+
if (!options.workspaceId)
|
|
2982
|
+
throw new ACPFailure("invalid");
|
|
2983
|
+
const workspace = await getWorkspace(options.workspaceId);
|
|
2984
|
+
if (agent.mode === "embedded" && workspace.storage.type !== "directory" || agent.mode === "builtin-service" && (workspace.storage.type !== "volume" || workspace.storage.image !== agent.image)) {
|
|
2985
|
+
throw new ACPFailure("unsupported");
|
|
2986
|
+
}
|
|
2987
|
+
return workspaceOperation(workspace, async (runtime) => {
|
|
2988
|
+
const info = {
|
|
2989
|
+
id: randomUUID(),
|
|
2990
|
+
agentId: id,
|
|
2991
|
+
cwd: runtime.root,
|
|
2992
|
+
newWorktree: false,
|
|
2993
|
+
agent,
|
|
2994
|
+
status: "opening",
|
|
2995
|
+
mcpServers: [],
|
|
2996
|
+
modelId: agent.defaultModelId ?? agent.modelIds[0]
|
|
2997
|
+
};
|
|
2998
|
+
const connection2 = await connectACP(
|
|
2999
|
+
await launchAgent(agent, runtime, info),
|
|
3000
|
+
options,
|
|
3001
|
+
limits
|
|
3002
|
+
);
|
|
3003
|
+
try {
|
|
3004
|
+
return connection2.initialized;
|
|
3005
|
+
} finally {
|
|
3006
|
+
await connection2.close();
|
|
3007
|
+
}
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
const connection = await connectACP(
|
|
3011
|
+
{
|
|
3012
|
+
type: agent.transport,
|
|
3013
|
+
url: agent.url,
|
|
3014
|
+
headers: resolveEnvironment(agent.headerEnv)
|
|
3015
|
+
},
|
|
3016
|
+
options,
|
|
3017
|
+
limits
|
|
3018
|
+
);
|
|
3019
|
+
try {
|
|
3020
|
+
return connection.initialized;
|
|
3021
|
+
} finally {
|
|
3022
|
+
await connection.close();
|
|
3023
|
+
}
|
|
3024
|
+
})
|
|
3025
|
+
},
|
|
3026
|
+
workspaces: {
|
|
3027
|
+
/**
|
|
3028
|
+
* 新增数据库工作区并归一化存储位置;拒绝目录重叠或同一 volume 重复登记。
|
|
3029
|
+
* @param input - 工作区存储位置、显式 Git 主分支和环境变量引用。
|
|
3030
|
+
* @returns 带来源和版本的工作区条目;重叠位置返回冲突错误。
|
|
3031
|
+
* @example
|
|
3032
|
+
* ```ts
|
|
3033
|
+
* import { ai } from '@h-ai/ai'
|
|
3034
|
+
* // 前提:ai 已成功初始化。
|
|
3035
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3036
|
+
* const result = await ai.acp.workspaces.register({ id: 'project', storage: { type: 'directory', path: '/srv/project' } })
|
|
3037
|
+
* ```
|
|
3038
|
+
*/
|
|
3039
|
+
register: (input) => protectResult(async () => {
|
|
3040
|
+
const workspace = ACPWorkspaceSchema.parse(input);
|
|
3041
|
+
if ((await workspaceRegistry.read(workspace.id))?.source === "config")
|
|
3042
|
+
return workspaceRegistry.operations.register(workspace);
|
|
3043
|
+
await normalizeWorkspace(workspace);
|
|
3044
|
+
const release = await acquire("definition-registry");
|
|
3045
|
+
try {
|
|
3046
|
+
await checkWorkspaceOverlap(workspace);
|
|
3047
|
+
return await workspaceRegistry.operations.register(workspace);
|
|
3048
|
+
} finally {
|
|
3049
|
+
await release();
|
|
3050
|
+
}
|
|
3051
|
+
}),
|
|
3052
|
+
/**
|
|
3053
|
+
* 读取工作区定义及来源;配置条目无数据库版本。
|
|
3054
|
+
* @param id - 工作区 ID。
|
|
3055
|
+
* @returns 工作区资源条目;未找到时返回资源不存在错误。
|
|
3056
|
+
* @example
|
|
3057
|
+
* ```ts
|
|
3058
|
+
* import { ai } from '@h-ai/ai'
|
|
3059
|
+
* // 前提:ai 已成功初始化。
|
|
3060
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3061
|
+
* const result = await ai.acp.workspaces.get('project')
|
|
3062
|
+
* ```
|
|
3063
|
+
*/
|
|
3064
|
+
get: workspaceRegistry.operations.get,
|
|
3065
|
+
/**
|
|
3066
|
+
* 列出配置与数据库合并后的工作区;不创建路径或克隆工程。
|
|
3067
|
+
* @returns 去重后的工作区列表;列表项不含数据库 version。
|
|
3068
|
+
* @example
|
|
3069
|
+
* ```ts
|
|
3070
|
+
* import { ai } from '@h-ai/ai'
|
|
3071
|
+
* // 前提:ai 已成功初始化。
|
|
3072
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3073
|
+
* const result = await ai.acp.workspaces.list()
|
|
3074
|
+
* ```
|
|
3075
|
+
*/
|
|
3076
|
+
list: workspaceRegistry.operations.list,
|
|
3077
|
+
/**
|
|
3078
|
+
* 整体替换无会话引用的数据库工作区,重新检查存储位置重叠;配置条目不可修改。
|
|
3079
|
+
* @param input - 包含原 ID 的完整替换定义。
|
|
3080
|
+
* @param version - 可选的数据库期望版本。
|
|
3081
|
+
* @returns 更新后的工作区;有会话引用、位置冲突或版本冲突时失败。
|
|
3082
|
+
* @example
|
|
3083
|
+
* ```ts
|
|
3084
|
+
* import { ai } from '@h-ai/ai'
|
|
3085
|
+
* // 前提:ai 已成功初始化。
|
|
3086
|
+
* // definition 为完整替换定义;entry 为 get 成功返回的条目。
|
|
3087
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3088
|
+
* const result = await ai.acp.workspaces.update(definition, entry.version)
|
|
3089
|
+
* ```
|
|
3090
|
+
*/
|
|
3091
|
+
update: (input, version) => protectResult(async () => {
|
|
3092
|
+
const workspace = ACPWorkspaceSchema.parse(input);
|
|
3093
|
+
const existing = await workspaceRegistry.read(workspace.id);
|
|
3094
|
+
if (existing?.source === "config")
|
|
3095
|
+
return workspaceRegistry.operations.update(workspace, version);
|
|
3096
|
+
const release = await acquire("definition-registry");
|
|
3097
|
+
try {
|
|
3098
|
+
if ((await sessions.query({ where: { workspaceId: workspace.id } })).length) {
|
|
3099
|
+
throw new ACPFailure("busy");
|
|
3100
|
+
}
|
|
3101
|
+
await normalizeWorkspace(workspace);
|
|
3102
|
+
await checkWorkspaceOverlap(workspace);
|
|
3103
|
+
return await workspaceRegistry.operations.update(
|
|
3104
|
+
workspace,
|
|
3105
|
+
version
|
|
3106
|
+
);
|
|
3107
|
+
} finally {
|
|
3108
|
+
await release();
|
|
3109
|
+
}
|
|
3110
|
+
}),
|
|
3111
|
+
/**
|
|
3112
|
+
* 创建目录并克隆缺失工程;已有内容不自动 pull 或覆盖,Git 不存在时返回明确错误。
|
|
3113
|
+
* @param id - 已登记工作区 ID。
|
|
3114
|
+
* @returns 准备后的 main 绝对目录;克隆失败保留现场供检查。
|
|
3115
|
+
* @example
|
|
3116
|
+
* ```ts
|
|
3117
|
+
* import { ai } from '@h-ai/ai'
|
|
3118
|
+
* // 前提:ai 已成功初始化。
|
|
3119
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3120
|
+
* const result = await ai.acp.workspaces.prepare('project')
|
|
3121
|
+
* ```
|
|
3122
|
+
*/
|
|
3123
|
+
prepare: (id) => protect(
|
|
3124
|
+
async () => workspaceOperation(await getWorkspace(id), (runtime) => createGitOperationsInput(id, runtime))
|
|
3125
|
+
),
|
|
3126
|
+
/**
|
|
3127
|
+
* 将有限大小的文件写入 main/files,拒绝覆盖、路径逃逸和符号链接;失败前已写入文件会保留。
|
|
3128
|
+
* @param id - 已登记工作区 ID。
|
|
3129
|
+
* @param files - 相对路径和 UTF-8 文本或二进制;不允许覆盖或路径逃逸。
|
|
3130
|
+
* @returns 已导入相对路径列表;写入中途失败不回滚已有文件。
|
|
3131
|
+
* @example
|
|
3132
|
+
* ```ts
|
|
3133
|
+
* import { ai } from '@h-ai/ai'
|
|
3134
|
+
* // 前提:ai 已成功初始化。
|
|
3135
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3136
|
+
* const result = await ai.acp.workspaces.importFiles('project', [{ path: 'notes.txt', content: '需求' }])
|
|
3137
|
+
* ```
|
|
3138
|
+
*/
|
|
3139
|
+
importFiles: (id, files) => protect(async () => {
|
|
3140
|
+
if (files.length > 1e3 || files.reduce(
|
|
3141
|
+
(sum, file) => sum + Buffer.byteLength(file.content),
|
|
3142
|
+
0
|
|
3143
|
+
) > limits.maxContentBytes) {
|
|
3144
|
+
throw new ACPFailure("output");
|
|
3145
|
+
}
|
|
3146
|
+
for (const file of files) {
|
|
3147
|
+
if (!file.path || file.path.split(/[\\/]/).some(
|
|
3148
|
+
(part) => !part || part === "." || part === ".." || part === ".git"
|
|
3149
|
+
) || isAbsolute(file.path)) {
|
|
3150
|
+
throw new ACPFailure("invalid");
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
return workspaceOperation(await getWorkspace(id), async (runtime) => {
|
|
3154
|
+
await runtime.file("mkdir", "main/files");
|
|
3155
|
+
for (const file of files) {
|
|
3156
|
+
if (await runtime.file("exists", `main/files/${file.path}`) === "true") {
|
|
3157
|
+
throw new ACPFailure("conflict");
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
for (const file of files) {
|
|
3161
|
+
await runtime.file(
|
|
3162
|
+
"write",
|
|
3163
|
+
`main/files/${file.path}`,
|
|
3164
|
+
Buffer.from(file.content).toString("base64")
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
3167
|
+
return { imported: files.map((file) => file.path) };
|
|
3168
|
+
});
|
|
3169
|
+
}),
|
|
3170
|
+
/**
|
|
3171
|
+
* 移除无会话引用的数据库定义,保留磁盘目录、Git 检出和 volume。
|
|
3172
|
+
* @param id - 数据库工作区 ID。
|
|
3173
|
+
* @param version - 可选的数据库期望版本。
|
|
3174
|
+
* @returns 删除定义的 HaiResult;引用存在时失败,文件始终保留。
|
|
3175
|
+
* @example
|
|
3176
|
+
* ```ts
|
|
3177
|
+
* import { ai } from '@h-ai/ai'
|
|
3178
|
+
* // 前提:ai 已成功初始化。
|
|
3179
|
+
* // entry 为 get 成功返回的数据库条目。
|
|
3180
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3181
|
+
* const result = await ai.acp.workspaces.remove('project', entry.version)
|
|
3182
|
+
* ```
|
|
3183
|
+
*/
|
|
3184
|
+
remove: (id, version) => protectResult(async () => {
|
|
3185
|
+
return withDefinitions(async () => {
|
|
3186
|
+
const existing = await workspaceRegistry.read(id);
|
|
3187
|
+
if (existing?.source === "config")
|
|
3188
|
+
return workspaceRegistry.operations.remove(id, version);
|
|
3189
|
+
if ((await sessions.query({ where: { workspaceId: id } })).length)
|
|
3190
|
+
throw new ACPFailure("busy");
|
|
3191
|
+
return workspaceRegistry.operations.remove(id, version);
|
|
3192
|
+
});
|
|
3193
|
+
})
|
|
3194
|
+
},
|
|
3195
|
+
sessions: {
|
|
3196
|
+
/**
|
|
3197
|
+
* 绑定模型、工作区和 MCP,准备目录后建立原生会话;newWorktree 可选择独立工程分支。
|
|
3198
|
+
* @param options - 智能体、工作区、模型、MCP 及可信回调;newWorktree 默认 false。
|
|
3199
|
+
* @returns 可用于 prompt/stream/publish 的会话句柄;启动失败保留失败记录。
|
|
3200
|
+
* @example
|
|
3201
|
+
* ```ts
|
|
3202
|
+
* import { ai } from '@h-ai/ai'
|
|
3203
|
+
* // 前提:ai 已成功初始化。
|
|
3204
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3205
|
+
* const result = await ai.acp.sessions.create({ agentId: 'coder', workspaceId: 'project', newWorktree: true })
|
|
3206
|
+
* ```
|
|
3207
|
+
*/
|
|
3208
|
+
create: (options) => protect(async () => {
|
|
3209
|
+
const info = await withDefinitions(async () => {
|
|
3210
|
+
const agent = await getAgent(options.agentId);
|
|
3211
|
+
await validateAgent(agent);
|
|
3212
|
+
if (!agent.enabled)
|
|
3213
|
+
throw new ACPFailure("unsupported");
|
|
3214
|
+
const id = sessionIdentitySchema.parse(options.id ?? randomUUID());
|
|
3215
|
+
const modelId = options.modelId ?? agent.defaultModelId ?? agent.modelIds[0];
|
|
3216
|
+
if (modelId && !agent.modelIds.includes(modelId))
|
|
3217
|
+
throw new ACPFailure("model");
|
|
3218
|
+
const info2 = {
|
|
3219
|
+
id,
|
|
3220
|
+
agentId: agent.id,
|
|
3221
|
+
workspaceId: options.workspaceId,
|
|
3222
|
+
cwd: options.cwd ?? "",
|
|
3223
|
+
newWorktree: options.newWorktree ?? false,
|
|
3224
|
+
branch: options.newWorktree ? branchNameSchema.parse(options.branch ?? `acp/${id}`) : void 0,
|
|
3225
|
+
modelId,
|
|
3226
|
+
modelSignature: await modelSignature(agent),
|
|
3227
|
+
status: "opening",
|
|
3228
|
+
agent,
|
|
3229
|
+
mcpServers: options.mcpServers ?? [],
|
|
3230
|
+
mcpServerIds: options.mcpServerIds
|
|
3231
|
+
};
|
|
3232
|
+
if (options.workspaceId)
|
|
3233
|
+
await getWorkspace(options.workspaceId);
|
|
3234
|
+
await unique(sessions, id, info2);
|
|
3235
|
+
return info2;
|
|
3236
|
+
});
|
|
3237
|
+
return start(info, options, false);
|
|
3238
|
+
}),
|
|
3239
|
+
/**
|
|
3240
|
+
* 获取持久会话的管理句柄;已关闭会话需要 resume 后才能执行 prompt。
|
|
3241
|
+
* @param id - 框架会话 ID,不是智能体原生 ID。
|
|
3242
|
+
* @returns 管理句柄的 HaiResult;不会自动重新启动智能体。
|
|
3243
|
+
* @example
|
|
3244
|
+
* ```ts
|
|
3245
|
+
* import { ai } from '@h-ai/ai'
|
|
3246
|
+
* // 前提:ai 已成功初始化。
|
|
3247
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3248
|
+
* const result = await ai.acp.sessions.get('task-1')
|
|
3249
|
+
* ```
|
|
3250
|
+
*/
|
|
3251
|
+
get: (id) => protect(async () => {
|
|
3252
|
+
await get(sessions, id);
|
|
3253
|
+
return handle(id);
|
|
3254
|
+
}),
|
|
3255
|
+
/**
|
|
3256
|
+
* 列出持久会话,可按工作区 ID 过滤。
|
|
3257
|
+
* @param workspaceId - 可选工作区筛选条件;省略返回全部会话。
|
|
3258
|
+
* @returns 持久 ACPSessionInfo 列表的 HaiResult。
|
|
3259
|
+
* @example
|
|
3260
|
+
* ```ts
|
|
3261
|
+
* import { ai } from '@h-ai/ai'
|
|
3262
|
+
* // 前提:ai 已成功初始化。
|
|
3263
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3264
|
+
* const result = await ai.acp.sessions.list('project')
|
|
3265
|
+
* ```
|
|
3266
|
+
*/
|
|
3267
|
+
list: (workspaceId) => protect(
|
|
3268
|
+
() => sessions.query({ where: workspaceId ? { workspaceId } : {} })
|
|
3269
|
+
),
|
|
3270
|
+
/**
|
|
3271
|
+
* 使用原生会话 ID 和保留状态恢复;智能体定义或模型能力已改变时拒绝恢复。
|
|
3272
|
+
* @param id - 已关闭或失败的框架会话 ID。
|
|
3273
|
+
* @param options - 本次连接的回调、能力和取消信号,不从数据库恢复回调。
|
|
3274
|
+
* @returns 恢复后的会话句柄;原生服务不支持恢复或配置变化时失败。
|
|
3275
|
+
* @example
|
|
3276
|
+
* ```ts
|
|
3277
|
+
* import { ai } from '@h-ai/ai'
|
|
3278
|
+
* // 前提:ai 已成功初始化。
|
|
3279
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3280
|
+
* const result = await ai.acp.sessions.resume('task-1')
|
|
3281
|
+
* ```
|
|
3282
|
+
*/
|
|
3283
|
+
resume: (id, options = {}) => protect(async () => {
|
|
3284
|
+
const info = await get(sessions, id);
|
|
3285
|
+
if (info.status === "opening" || info.status === "active")
|
|
3286
|
+
throw new ACPFailure("busy");
|
|
3287
|
+
const agent = await getAgent(info.agentId);
|
|
3288
|
+
if (!agent.enabled || JSON.stringify(agent) !== JSON.stringify(info.agent)) {
|
|
3289
|
+
throw new ACPFailure("conflict");
|
|
3290
|
+
}
|
|
3291
|
+
if (info.modelSignature !== await modelSignature(agent))
|
|
3292
|
+
throw new ACPFailure("conflict");
|
|
3293
|
+
return start(info, options, true);
|
|
3294
|
+
}),
|
|
3295
|
+
/**
|
|
3296
|
+
* 关闭本进程拥有的会话并释放锁,不删除历史和工程内容。
|
|
3297
|
+
* @param id - 框架会话 ID。
|
|
3298
|
+
* @returns 关闭完成的 HaiResult;不允许关闭其他进程拥有的执行环境。
|
|
3299
|
+
* @example
|
|
3300
|
+
* ```ts
|
|
3301
|
+
* import { ai } from '@h-ai/ai'
|
|
3302
|
+
* // 前提:ai 已成功初始化。
|
|
3303
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3304
|
+
* const result = await ai.acp.sessions.close('task-1')
|
|
3305
|
+
* ```
|
|
3306
|
+
*/
|
|
3307
|
+
close: (id) => protect(() => stop(id)),
|
|
3308
|
+
/**
|
|
3309
|
+
* 仅删除已关闭或失败的会话记录;不删除工作树及原生文件,不替代 Git worktree 清理。
|
|
3310
|
+
* @param id - 框架会话 ID。
|
|
3311
|
+
* @returns 删除完成的 HaiResult;活动会话返回忙碌错误。
|
|
3312
|
+
* @example
|
|
3313
|
+
* ```ts
|
|
3314
|
+
* import { ai } from '@h-ai/ai'
|
|
3315
|
+
* // 前提:ai 已成功初始化。
|
|
3316
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3317
|
+
* const result = await ai.acp.sessions.remove('task-1')
|
|
3318
|
+
* ```
|
|
3319
|
+
*/
|
|
3320
|
+
remove: (id) => protect(async () => {
|
|
3321
|
+
const info = await get(sessions, id);
|
|
3322
|
+
if (live.has(id) || info.status === "active" || info.status === "opening") {
|
|
3323
|
+
throw new ACPFailure("busy");
|
|
3324
|
+
}
|
|
3325
|
+
const release = await acquire(`session:${id}`);
|
|
3326
|
+
try {
|
|
3327
|
+
await sessions.remove(id);
|
|
3328
|
+
} finally {
|
|
3329
|
+
await release();
|
|
3330
|
+
}
|
|
3331
|
+
})
|
|
3332
|
+
},
|
|
3333
|
+
/**
|
|
3334
|
+
* 连接原生 stdio/HTTP/WS 服务;不创建工作区或提供传输兼容桥。
|
|
3335
|
+
* @param transport - 智能体实际支持的原生传输与命令或地址。
|
|
3336
|
+
* @param options - 可信回调、声明能力和初始化取消信号。
|
|
3337
|
+
* @returns 低层连接句柄的 HaiResult;连接失败不自动转换传输。
|
|
3338
|
+
* @example
|
|
3339
|
+
* ```ts
|
|
3340
|
+
* import { ai } from '@h-ai/ai'
|
|
3341
|
+
* // 前提:ai 已成功初始化。
|
|
3342
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3343
|
+
* const result = await ai.acp.connect({ type: 'http', url: 'https://agent.example.com/acp' })
|
|
3344
|
+
* ```
|
|
3345
|
+
*/
|
|
3346
|
+
connect: (transport, options = {}) => protect(async () => {
|
|
3347
|
+
const connection = await connectACP(transport, options, limits);
|
|
3348
|
+
connections.add(connection);
|
|
3349
|
+
return {
|
|
3350
|
+
info: connection.initialized,
|
|
3351
|
+
/**
|
|
3352
|
+
* 选择对端握手公布的认证方法,返回 HaiResult。
|
|
3353
|
+
* @param method - initialize 返回的认证方法 ID。
|
|
3354
|
+
* @returns 认证响应的 HaiResult。
|
|
3355
|
+
* @example
|
|
3356
|
+
* ```ts
|
|
3357
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3358
|
+
* const result = await connection.authenticate('api-key')
|
|
3359
|
+
* ```
|
|
3360
|
+
*/
|
|
3361
|
+
authenticate: (method) => protect(() => connection.authenticate(method)),
|
|
3362
|
+
/**
|
|
3363
|
+
* 在对端可见的绝对 cwd 创建原生会话,返回 prompt/cancel 句柄。
|
|
3364
|
+
* @param cwd - 对端文件系统可见的绝对工作目录。
|
|
3365
|
+
* @param mcpServers - 可选原生 MCP 定义;调用方负责凭据与授权。
|
|
3366
|
+
* @returns 原生会话句柄;不创建框架持久会话记录。
|
|
3367
|
+
* @example
|
|
3368
|
+
* ```ts
|
|
3369
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3370
|
+
* const result = await connection.createSession('/srv/project', [])
|
|
3371
|
+
* ```
|
|
3372
|
+
*/
|
|
3373
|
+
createSession: (cwd, mcpServers) => protect(async () => {
|
|
3374
|
+
const session = await connection.createSession(cwd, mcpServers);
|
|
3375
|
+
return {
|
|
3376
|
+
id: session.sessionId,
|
|
3377
|
+
/**
|
|
3378
|
+
* 发送一轮原生内容,返回最终文本、停止原因和事件。
|
|
3379
|
+
* @param content - 原生 ACP 内容块。
|
|
3380
|
+
* @param signal - 可选本轮取消信号。
|
|
3381
|
+
* @returns 最终文本、停止原因和事件的 HaiResult。
|
|
3382
|
+
* @example
|
|
3383
|
+
* ```ts
|
|
3384
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3385
|
+
* const result = await session.prompt([{ type: 'text', text: '检查工程' }])
|
|
3386
|
+
* ```
|
|
3387
|
+
*/
|
|
3388
|
+
prompt: (content, signal) => protect(() => connection.prompt(content, signal)),
|
|
3389
|
+
/**
|
|
3390
|
+
* 取消当前轮次,超时则关闭连接。
|
|
3391
|
+
* @returns 取消完成的 HaiResult。
|
|
3392
|
+
* @example
|
|
3393
|
+
* ```ts
|
|
3394
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3395
|
+
* const result = await session.cancel()
|
|
3396
|
+
* ```
|
|
3397
|
+
*/
|
|
3398
|
+
cancel: () => protect(() => connection.cancel())
|
|
3399
|
+
};
|
|
3400
|
+
}),
|
|
3401
|
+
/**
|
|
3402
|
+
* 读取对端原生会话列表;未声明相应能力时返回错误。
|
|
3403
|
+
* @param cursor - 上一次原生列表响应中的分页游标。
|
|
3404
|
+
* @returns 原生分页响应的 HaiResult。
|
|
3405
|
+
* @example
|
|
3406
|
+
* ```ts
|
|
3407
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3408
|
+
* const result = await connection.listSessions()
|
|
3409
|
+
* ```
|
|
3410
|
+
*/
|
|
3411
|
+
listSessions: (cursor) => protect(() => connection.listSessions(cursor)),
|
|
3412
|
+
/**
|
|
3413
|
+
* 按原生 ID 删除远端会话;不会删除框架数据库会话记录。
|
|
3414
|
+
* @param id - 对端返回的原生会话 ID。
|
|
3415
|
+
* @returns 原生删除响应的 HaiResult;不支持时返回协议错误。
|
|
3416
|
+
* @example
|
|
3417
|
+
* ```ts
|
|
3418
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3419
|
+
* const result = await connection.deleteSession('native-id')
|
|
3420
|
+
* ```
|
|
3421
|
+
*/
|
|
3422
|
+
deleteSession: (id) => protect(() => connection.deleteSession(id)),
|
|
3423
|
+
/**
|
|
3424
|
+
* 关闭连接和拥有的子进程;不删除远端文件或持久状态。
|
|
3425
|
+
* @returns 关闭完成的 HaiResult;可重复关闭。
|
|
3426
|
+
* @example
|
|
3427
|
+
* ```ts
|
|
3428
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3429
|
+
* const result = await connection.close()
|
|
3430
|
+
* ```
|
|
3431
|
+
*/
|
|
3432
|
+
close: () => protect(async () => {
|
|
3433
|
+
await connection.close();
|
|
3434
|
+
connections.delete(connection);
|
|
3435
|
+
})
|
|
3436
|
+
};
|
|
3437
|
+
}),
|
|
3438
|
+
/**
|
|
3439
|
+
* 关闭子系统及拥有的资源;由 ai.close 生命周期调用。
|
|
3440
|
+
* @returns 所有已接受操作结束和拥有资源完成清理后的 Promise。
|
|
3441
|
+
* @throws 清理进程或容器失败时抛出 ACPFailure,由 ai.close 统一处理。
|
|
3442
|
+
* @example
|
|
3443
|
+
* ```ts
|
|
3444
|
+
* import { ai } from '@h-ai/ai'
|
|
3445
|
+
* // 前提:ai 已成功初始化。
|
|
3446
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3447
|
+
* await ai.close()
|
|
3448
|
+
* ```
|
|
3449
|
+
*/
|
|
3450
|
+
async close() {
|
|
3451
|
+
closed = true;
|
|
3452
|
+
await Promise.allSettled(
|
|
3453
|
+
[...live.values()].map((value) => value.connection.cancel())
|
|
3454
|
+
);
|
|
3455
|
+
await Promise.allSettled(
|
|
3456
|
+
[...connections].map((connection) => connection.close())
|
|
3457
|
+
);
|
|
3458
|
+
await Promise.allSettled([...pending]);
|
|
3459
|
+
const results = await Promise.allSettled([...live.keys()].map(stop));
|
|
3460
|
+
const more = await Promise.allSettled(
|
|
3461
|
+
[...connections].map((connection) => connection.close())
|
|
3462
|
+
);
|
|
3463
|
+
connections.clear();
|
|
3464
|
+
if ([...results, ...more].some((result) => result.status === "rejected"))
|
|
3465
|
+
throw new ACPFailure("cleanup");
|
|
3466
|
+
}
|
|
3467
|
+
};
|
|
3468
|
+
async function createGitOperationsInput(id, runtime) {
|
|
3469
|
+
return createGitOperations(await getWorkspace(id), runtime).prepare();
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
705
3472
|
var SENTENCE_BOUNDARY_REGEX = /[。!?;!?;\n]|\.(?=\s|$)/;
|
|
706
3473
|
async function* streamSentences(stream) {
|
|
707
3474
|
let buffer = "";
|
|
@@ -799,8 +3566,8 @@ async function describeHttpError(response) {
|
|
|
799
3566
|
}
|
|
800
3567
|
var logger3 = core.logger.child({ module: "ai", scope: "audio-ws" });
|
|
801
3568
|
function openAudioWebSocket(url, headers, options) {
|
|
802
|
-
return new Promise((
|
|
803
|
-
const ws = new
|
|
3569
|
+
return new Promise((resolve3, reject) => {
|
|
3570
|
+
const ws = new WebSocket2(url, { headers, handshakeTimeout: options?.timeout ?? 15e3 });
|
|
804
3571
|
ws.binaryType = "arraybuffer";
|
|
805
3572
|
const queue = [];
|
|
806
3573
|
let waiting = null;
|
|
@@ -903,7 +3670,7 @@ function openAudioWebSocket(url, headers, options) {
|
|
|
903
3670
|
}
|
|
904
3671
|
}
|
|
905
3672
|
};
|
|
906
|
-
|
|
3673
|
+
resolve3(connection);
|
|
907
3674
|
});
|
|
908
3675
|
});
|
|
909
3676
|
}
|
|
@@ -914,7 +3681,7 @@ var DOUBAO_CAPABILITIES = {
|
|
|
914
3681
|
transcribe: { supported: true, realtimeAudioInput: true, speechBoundaryEvents: false, streamingTranscriptOutput: true },
|
|
915
3682
|
synthesize: { supported: true, incrementalTextInput: true, streamingAudioOutput: true }
|
|
916
3683
|
};
|
|
917
|
-
var
|
|
3684
|
+
var PROTOCOL_VERSION2 = 1;
|
|
918
3685
|
var HEADER_SIZE = 1;
|
|
919
3686
|
var MSG_FULL_CLIENT = 1;
|
|
920
3687
|
var MSG_AUDIO_ONLY_CLIENT = 2;
|
|
@@ -952,7 +3719,7 @@ var DOUBAO_FORMAT = {
|
|
|
952
3719
|
};
|
|
953
3720
|
function buildHeader(messageType, flags, serialization, compression) {
|
|
954
3721
|
return Buffer.from([
|
|
955
|
-
|
|
3722
|
+
PROTOCOL_VERSION2 << 4 | HEADER_SIZE,
|
|
956
3723
|
messageType << 4 | flags,
|
|
957
3724
|
serialization << 4 | compression,
|
|
958
3725
|
0
|
|
@@ -1726,9 +4493,9 @@ function createOpenAIAudioProvider() {
|
|
|
1726
4493
|
if (!model.apiKey)
|
|
1727
4494
|
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_audioMissingApiKey", { params: { provider: "openai" } }));
|
|
1728
4495
|
try {
|
|
1729
|
-
const
|
|
4496
|
+
const client2 = createClient(model.apiKey, model.baseUrl, model.timeout);
|
|
1730
4497
|
const file = await toUploadFile(audio);
|
|
1731
|
-
const result = await
|
|
4498
|
+
const result = await client2.audio.transcriptions.create({
|
|
1732
4499
|
file,
|
|
1733
4500
|
model: model.model,
|
|
1734
4501
|
language,
|
|
@@ -1750,9 +4517,9 @@ function createOpenAIAudioProvider() {
|
|
|
1750
4517
|
aiM("ai_audioUnsupportedInput", { params: { provider: "openai", reason: "streaming audio input" } })
|
|
1751
4518
|
);
|
|
1752
4519
|
}
|
|
1753
|
-
const
|
|
4520
|
+
const client2 = createClient(model.apiKey, model.baseUrl, model.timeout);
|
|
1754
4521
|
const file = await toUploadFile(audio);
|
|
1755
|
-
const stream = await
|
|
4522
|
+
const stream = await client2.audio.transcriptions.create({
|
|
1756
4523
|
file,
|
|
1757
4524
|
model: model.model,
|
|
1758
4525
|
language,
|
|
@@ -1775,8 +4542,8 @@ function createOpenAIAudioProvider() {
|
|
|
1775
4542
|
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_audioMissingApiKey", { params: { provider: "openai" } }));
|
|
1776
4543
|
const out = resolveSynthesisOutput4({ format, sampleRate });
|
|
1777
4544
|
try {
|
|
1778
|
-
const
|
|
1779
|
-
const response = await
|
|
4545
|
+
const client2 = createClient(model.apiKey, model.baseUrl, model.timeout);
|
|
4546
|
+
const response = await client2.audio.speech.create({
|
|
1780
4547
|
model: model.model,
|
|
1781
4548
|
voice: voice ?? "alloy",
|
|
1782
4549
|
input: text,
|
|
@@ -1795,12 +4562,12 @@ function createOpenAIAudioProvider() {
|
|
|
1795
4562
|
if (!model.apiKey)
|
|
1796
4563
|
throw audioError(HaiAIError.CONFIGURATION_ERROR, aiM("ai_audioMissingApiKey", { params: { provider: "openai" } }));
|
|
1797
4564
|
const outFormat = resolveSynthesisOutput4({ format }).format;
|
|
1798
|
-
const
|
|
4565
|
+
const client2 = createClient(model.apiKey, model.baseUrl, model.timeout);
|
|
1799
4566
|
const segments = typeof text === "string" ? [text] : streamSentences(text);
|
|
1800
4567
|
for await (const segment of segments) {
|
|
1801
4568
|
if (!segment)
|
|
1802
4569
|
continue;
|
|
1803
|
-
const response = await
|
|
4570
|
+
const response = await client2.audio.speech.create({
|
|
1804
4571
|
model: model.model,
|
|
1805
4572
|
voice: voice ?? "alloy",
|
|
1806
4573
|
input: segment,
|
|
@@ -2854,12 +5621,12 @@ function createContextOperations(compressConfig, tokenOps, compressOps, contextS
|
|
|
2854
5621
|
delete candidate.active;
|
|
2855
5622
|
}
|
|
2856
5623
|
async function waitForContext(signal) {
|
|
2857
|
-
await new Promise((
|
|
5624
|
+
await new Promise((resolve3, reject) => {
|
|
2858
5625
|
const timer = setTimeout(finish, 50);
|
|
2859
5626
|
function finish() {
|
|
2860
5627
|
clearTimeout(timer);
|
|
2861
5628
|
signal?.removeEventListener("abort", abort);
|
|
2862
|
-
|
|
5629
|
+
resolve3();
|
|
2863
5630
|
}
|
|
2864
5631
|
function abort() {
|
|
2865
5632
|
clearTimeout(timer);
|
|
@@ -3579,7 +6346,7 @@ function createEmbeddingOperations(config) {
|
|
|
3579
6346
|
const resolved = resolvedResult.data;
|
|
3580
6347
|
const input = Array.isArray(request.input) ? request.input : [request.input];
|
|
3581
6348
|
try {
|
|
3582
|
-
const
|
|
6349
|
+
const client2 = getOrCreateClient(resolved);
|
|
3583
6350
|
const params = {
|
|
3584
6351
|
model: resolved.model,
|
|
3585
6352
|
input
|
|
@@ -3587,7 +6354,7 @@ function createEmbeddingOperations(config) {
|
|
|
3587
6354
|
if (request.dimensions ?? embeddingConfig.dimensions) {
|
|
3588
6355
|
params.dimensions = request.dimensions ?? embeddingConfig.dimensions;
|
|
3589
6356
|
}
|
|
3590
|
-
const response = await
|
|
6357
|
+
const response = await client2.embeddings.create(params);
|
|
3591
6358
|
return ok({
|
|
3592
6359
|
model: response.model,
|
|
3593
6360
|
data: response.data.map((item) => ({
|
|
@@ -5217,9 +7984,9 @@ ${contextText}
|
|
|
5217
7984
|
async ingestFile(input) {
|
|
5218
7985
|
try {
|
|
5219
7986
|
const { readFile } = await import('fs/promises');
|
|
5220
|
-
const { basename } = await import('path');
|
|
7987
|
+
const { basename: basename2 } = await import('path');
|
|
5221
7988
|
const content = await readFile(input.filePath, { encoding: input.encoding ?? "utf-8" });
|
|
5222
|
-
const fileName =
|
|
7989
|
+
const fileName = basename2(input.filePath);
|
|
5223
7990
|
const documentId = input.documentId ?? fileName;
|
|
5224
7991
|
return this.ingest({
|
|
5225
7992
|
documentId,
|
|
@@ -5294,13 +8061,13 @@ async function runWithCancellation(run, context) {
|
|
|
5294
8061
|
const { signal } = context;
|
|
5295
8062
|
if (signal.aborted)
|
|
5296
8063
|
throw new DOMException("Aborted", "AbortError");
|
|
5297
|
-
return await new Promise((
|
|
8064
|
+
return await new Promise((resolve3, reject) => {
|
|
5298
8065
|
const onAbort = () => reject(new DOMException("Aborted", "AbortError"));
|
|
5299
8066
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
5300
8067
|
Promise.resolve().then(() => run(context)).then(
|
|
5301
8068
|
(value) => {
|
|
5302
8069
|
signal.removeEventListener("abort", onAbort);
|
|
5303
|
-
|
|
8070
|
+
resolve3(value);
|
|
5304
8071
|
},
|
|
5305
8072
|
(error) => {
|
|
5306
8073
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -5612,11 +8379,11 @@ function createAnthropicProvider(deps) {
|
|
|
5612
8379
|
function resolveAnthropic(requestModel, tempModel) {
|
|
5613
8380
|
const entry = requestModel ? config.llm.models?.find((m) => m.id === requestModel || m.model === requestModel) : void 0;
|
|
5614
8381
|
const model = tempModel?.model ?? entry?.model ?? requestModel ?? config.llm.model ?? "claude-3-5-sonnet-latest";
|
|
5615
|
-
const apiKey = tempModel?.apiKey ?? entry?.apiKey ?? config.llm.apiKey ?? (
|
|
8382
|
+
const apiKey = tempModel?.apiKey ?? entry?.apiKey ?? config.llm.apiKey ?? (process2.env.HAI_AI_LLM_APIKEY?.trim() || process2.env.ANTHROPIC_API_KEY);
|
|
5616
8383
|
if (!apiKey)
|
|
5617
8384
|
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_configError", { params: { error: "API Key is required for Anthropic" } }));
|
|
5618
|
-
const globalBaseUrl = config.llm.
|
|
5619
|
-
const baseUrl = (tempModel?.baseUrl ?? entry?.baseUrl ?? globalBaseUrl ??
|
|
8385
|
+
const globalBaseUrl = config.llm.protocol === "anthropic" ? config.llm.baseUrl ?? (process2.env.HAI_AI_LLM_BASEURL?.trim() || void 0) : void 0;
|
|
8386
|
+
const baseUrl = (tempModel?.baseUrl ?? entry?.baseUrl ?? globalBaseUrl ?? process2.env.ANTHROPIC_BASE_URL ?? DEFAULT_ANTHROPIC_BASE_URL).replace(/\/+$/, "");
|
|
5620
8387
|
return ok({
|
|
5621
8388
|
apiKey,
|
|
5622
8389
|
baseUrl,
|
|
@@ -5729,7 +8496,7 @@ function createAnthropicProvider(deps) {
|
|
|
5729
8496
|
if (config.llm.model)
|
|
5730
8497
|
models.add(config.llm.model);
|
|
5731
8498
|
for (const entry of config.llm.models ?? []) {
|
|
5732
|
-
if ((entry.
|
|
8499
|
+
if ((entry.protocol ?? config.llm.protocol) === "anthropic")
|
|
5733
8500
|
models.add(entry.model);
|
|
5734
8501
|
}
|
|
5735
8502
|
return ok([...models]);
|
|
@@ -5926,12 +8693,12 @@ function createOpenAIProvider(deps) {
|
|
|
5926
8693
|
const tempCacheTtl = config.llm.tempModelCacheTtl ?? 6e5;
|
|
5927
8694
|
function getClient(apiKey, baseURL, timeout) {
|
|
5928
8695
|
const key = `${apiKey}::${baseURL}::${timeout}`;
|
|
5929
|
-
let
|
|
5930
|
-
if (!
|
|
5931
|
-
|
|
5932
|
-
clientCache.set(key,
|
|
8696
|
+
let client2 = clientCache.get(key);
|
|
8697
|
+
if (!client2) {
|
|
8698
|
+
client2 = new OpenAI3({ apiKey, baseURL, timeout });
|
|
8699
|
+
clientCache.set(key, client2);
|
|
5933
8700
|
}
|
|
5934
|
-
return
|
|
8701
|
+
return client2;
|
|
5935
8702
|
}
|
|
5936
8703
|
function getTempClient(apiKey, baseURL, timeout) {
|
|
5937
8704
|
const now = Date.now();
|
|
@@ -5943,9 +8710,9 @@ function createOpenAIProvider(deps) {
|
|
|
5943
8710
|
const cached = tempClientCache.get(key);
|
|
5944
8711
|
if (cached)
|
|
5945
8712
|
return cached.client;
|
|
5946
|
-
const
|
|
5947
|
-
tempClientCache.set(key, { client, expiresAt: now + tempCacheTtl });
|
|
5948
|
-
return
|
|
8713
|
+
const client2 = new OpenAI3({ apiKey, baseURL, timeout });
|
|
8714
|
+
tempClientCache.set(key, { client: client2, expiresAt: now + tempCacheTtl });
|
|
8715
|
+
return client2;
|
|
5949
8716
|
}
|
|
5950
8717
|
function resolveClient(requestModel, tempModel) {
|
|
5951
8718
|
if (tempModel)
|
|
@@ -5955,21 +8722,21 @@ function createOpenAIProvider(deps) {
|
|
|
5955
8722
|
});
|
|
5956
8723
|
if (!resolvedResult.success)
|
|
5957
8724
|
return resolvedResult;
|
|
5958
|
-
const { apiKey, baseUrl, timeout, model, api } = resolvedResult.data;
|
|
8725
|
+
const { apiKey, baseUrl, timeout, model, protocol: api } = resolvedResult.data;
|
|
5959
8726
|
const resolvedApi = api === "responses" ? "responses" : "chat";
|
|
5960
|
-
return ok({ client: getClient(apiKey ?? "", baseUrl ?? "https://api.openai.com/v1", timeout), model,
|
|
8727
|
+
return ok({ client: getClient(apiKey ?? "", baseUrl ?? "https://api.openai.com/v1", timeout), model, protocol: resolvedApi });
|
|
5961
8728
|
}
|
|
5962
8729
|
function resolveTempClient(tempModel) {
|
|
5963
|
-
const apiKey = tempModel.apiKey ?? config.llm.apiKey ?? (
|
|
8730
|
+
const apiKey = tempModel.apiKey ?? config.llm.apiKey ?? (process2.env.HAI_AI_LLM_APIKEY?.trim() || process2.env.OPENAI_API_KEY);
|
|
5964
8731
|
if (!apiKey)
|
|
5965
8732
|
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_configError", { params: { error: "API Key is required for temp model" } }));
|
|
5966
|
-
const baseUrl = tempModel.baseUrl ?? config.llm.baseUrl ?? (
|
|
8733
|
+
const baseUrl = tempModel.baseUrl ?? config.llm.baseUrl ?? (process2.env.HAI_AI_LLM_BASEURL?.trim() || process2.env.OPENAI_BASE_URL) ?? "https://api.openai.com/v1";
|
|
5967
8734
|
const timeout = tempModel.timeout ?? config.llm.timeout ?? 6e4;
|
|
5968
|
-
const api = (tempModel.
|
|
8735
|
+
const api = (tempModel.protocol ?? config.llm.protocol) === "responses" ? "responses" : "chat";
|
|
5969
8736
|
return ok({
|
|
5970
8737
|
client: getTempClient(apiKey, baseUrl, timeout),
|
|
5971
8738
|
model: tempModel.model,
|
|
5972
|
-
api,
|
|
8739
|
+
protocol: api,
|
|
5973
8740
|
maxTokens: tempModel.maxTokens ?? config.llm.maxTokens,
|
|
5974
8741
|
temperature: tempModel.temperature ?? config.llm.temperature
|
|
5975
8742
|
});
|
|
@@ -5995,11 +8762,11 @@ function createOpenAIProvider(deps) {
|
|
|
5995
8762
|
if (!clientResult.success)
|
|
5996
8763
|
return clientResult;
|
|
5997
8764
|
const resolved = clientResult.data;
|
|
5998
|
-
const { client, model, maxTokens, temperature, api } = resolved;
|
|
8765
|
+
const { client: client2, model, maxTokens, temperature, protocol: api } = resolved;
|
|
5999
8766
|
const { objectId: _objectId, sessionId: _sessionId, tempModel: _tempModel, signal, ...openaiRequest } = request;
|
|
6000
8767
|
try {
|
|
6001
8768
|
if (api === "responses") {
|
|
6002
|
-
const response2 = await
|
|
8769
|
+
const response2 = await client2.responses.create(buildResponsesParams(request, resolved), { signal });
|
|
6003
8770
|
return ok(responsesToChatResponse(response2, model));
|
|
6004
8771
|
}
|
|
6005
8772
|
const openaiMessages = request.messages.map(toOpenAIMessage);
|
|
@@ -6011,7 +8778,7 @@ function createOpenAIProvider(deps) {
|
|
|
6011
8778
|
...temperature !== void 0 && openaiRequest.temperature == null ? { temperature } : {},
|
|
6012
8779
|
stream: false
|
|
6013
8780
|
};
|
|
6014
|
-
const response = signal ? await
|
|
8781
|
+
const response = signal ? await client2.chat.completions.create(params, { signal }) : await client2.chat.completions.create(params);
|
|
6015
8782
|
return ok(response);
|
|
6016
8783
|
} catch (error) {
|
|
6017
8784
|
const mapped = toAIError(error);
|
|
@@ -6023,10 +8790,10 @@ function createOpenAIProvider(deps) {
|
|
|
6023
8790
|
if (!clientResult.success)
|
|
6024
8791
|
throw new Error(clientResult.error.message);
|
|
6025
8792
|
const resolved = clientResult.data;
|
|
6026
|
-
const { client, model, maxTokens, temperature, api } = resolved;
|
|
8793
|
+
const { client: client2, model, maxTokens, temperature, protocol: api } = resolved;
|
|
6027
8794
|
const { objectId: _objectId, sessionId: _sessionId, tempModel: _tempModel, signal, ...openaiRequest } = request;
|
|
6028
8795
|
if (api === "responses") {
|
|
6029
|
-
const responsesStream = await
|
|
8796
|
+
const responsesStream = await client2.responses.create(
|
|
6030
8797
|
{ ...buildResponsesParams(request, resolved), stream: true },
|
|
6031
8798
|
{ signal }
|
|
6032
8799
|
);
|
|
@@ -6046,7 +8813,7 @@ function createOpenAIProvider(deps) {
|
|
|
6046
8813
|
...temperature !== void 0 && openaiRequest.temperature == null ? { temperature } : {},
|
|
6047
8814
|
stream: true
|
|
6048
8815
|
};
|
|
6049
|
-
const stream = signal ? await
|
|
8816
|
+
const stream = signal ? await client2.chat.completions.create(params, { signal }) : await client2.chat.completions.create(params);
|
|
6050
8817
|
for await (const chunk of stream) {
|
|
6051
8818
|
yield chunk;
|
|
6052
8819
|
}
|
|
@@ -6055,9 +8822,9 @@ function createOpenAIProvider(deps) {
|
|
|
6055
8822
|
const clientResult = resolveClient();
|
|
6056
8823
|
if (!clientResult.success)
|
|
6057
8824
|
return clientResult;
|
|
6058
|
-
const { client } = clientResult.data;
|
|
8825
|
+
const { client: client2 } = clientResult.data;
|
|
6059
8826
|
try {
|
|
6060
|
-
const response = await
|
|
8827
|
+
const response = await client2.models.list();
|
|
6061
8828
|
const models = response.data.map((m) => m.id);
|
|
6062
8829
|
return ok(models);
|
|
6063
8830
|
} catch (error) {
|
|
@@ -6077,14 +8844,14 @@ function createLLMProvider(deps) {
|
|
|
6077
8844
|
return anthropic;
|
|
6078
8845
|
}
|
|
6079
8846
|
function pick(request) {
|
|
6080
|
-
const api = resolveModelApi(deps.config.llm, request.model, request.tempModel?.
|
|
8847
|
+
const api = resolveModelApi(deps.config.llm, request.model, request.tempModel?.protocol);
|
|
6081
8848
|
return api === "anthropic" ? getAnthropic() : openai;
|
|
6082
8849
|
}
|
|
6083
8850
|
return {
|
|
6084
8851
|
chat: (request) => pick(request).chat(request),
|
|
6085
8852
|
chatStream: (request) => pick(request).chatStream(request),
|
|
6086
8853
|
listModels: () => {
|
|
6087
|
-
const api = deps.config.llm.
|
|
8854
|
+
const api = deps.config.llm.protocol ?? "chat";
|
|
6088
8855
|
return api === "anthropic" ? getAnthropic().listModels() : openai.listModels();
|
|
6089
8856
|
}
|
|
6090
8857
|
};
|
|
@@ -6093,7 +8860,19 @@ function createLLMProvider(deps) {
|
|
|
6093
8860
|
// src/llm/ai-llm-functions.ts
|
|
6094
8861
|
var logger18 = core.logger.child({ module: "ai", scope: "llm" });
|
|
6095
8862
|
function createAILLMFunctions(config, deps) {
|
|
6096
|
-
|
|
8863
|
+
let provider = createLLMProvider({ config });
|
|
8864
|
+
let signature = JSON.stringify(config.llm);
|
|
8865
|
+
async function currentProvider() {
|
|
8866
|
+
if (!deps?.resolveConfig)
|
|
8867
|
+
return provider;
|
|
8868
|
+
const snapshot2 = await deps.resolveConfig();
|
|
8869
|
+
const next = JSON.stringify(snapshot2.llm);
|
|
8870
|
+
if (next !== signature) {
|
|
8871
|
+
provider = createLLMProvider({ config: snapshot2 });
|
|
8872
|
+
signature = next;
|
|
8873
|
+
}
|
|
8874
|
+
return provider;
|
|
8875
|
+
}
|
|
6097
8876
|
const recordStore = deps?.recordStore;
|
|
6098
8877
|
const sessionStore = deps?.sessionStore;
|
|
6099
8878
|
let recordSeq = 0;
|
|
@@ -6108,21 +8887,47 @@ function createAILLMFunctions(config, deps) {
|
|
|
6108
8887
|
return "New Session";
|
|
6109
8888
|
}
|
|
6110
8889
|
async function saveRecordAndSession(record, messages) {
|
|
6111
|
-
await recordStore.save(record.id, record, {
|
|
6112
|
-
|
|
8890
|
+
await recordStore.save(record.id, record, {
|
|
8891
|
+
objectId: record.objectId,
|
|
8892
|
+
sessionId: record.sessionId
|
|
8893
|
+
});
|
|
8894
|
+
logger18.trace("Chat record saved", {
|
|
8895
|
+
id: record.id,
|
|
8896
|
+
objectId: record.objectId,
|
|
8897
|
+
sessionId: record.sessionId
|
|
8898
|
+
});
|
|
6113
8899
|
if (sessionStore) {
|
|
6114
8900
|
await updateSessionInfo(sessionStore, record, (existing, now) => {
|
|
6115
8901
|
if (existing?.deleted)
|
|
6116
8902
|
return void 0;
|
|
6117
|
-
return existing ? { ...existing, updatedAt: now } : {
|
|
8903
|
+
return existing ? { ...existing, updatedAt: now } : {
|
|
8904
|
+
sessionId: record.sessionId,
|
|
8905
|
+
objectId: record.objectId,
|
|
8906
|
+
title: extractTitle(messages),
|
|
8907
|
+
createdAt: now,
|
|
8908
|
+
updatedAt: now
|
|
8909
|
+
};
|
|
6118
8910
|
});
|
|
6119
8911
|
}
|
|
6120
8912
|
}
|
|
6121
8913
|
async function chatWithRecord(request) {
|
|
8914
|
+
if (request.tempModel && Object.hasOwn(request.tempModel, "api"))
|
|
8915
|
+
return err(HaiAIError.INVALID_REQUEST, aiM("ai_llmProtocolRenamed"));
|
|
6122
8916
|
const start = Date.now();
|
|
6123
|
-
const context = {
|
|
8917
|
+
const context = {
|
|
8918
|
+
model: request.model ?? request.tempModel?.model,
|
|
8919
|
+
messageCount: request.messages.length
|
|
8920
|
+
};
|
|
6124
8921
|
logger18.debug("LLM chat started", context);
|
|
6125
|
-
|
|
8922
|
+
let result;
|
|
8923
|
+
try {
|
|
8924
|
+
result = await (await currentProvider()).chat(request);
|
|
8925
|
+
} catch {
|
|
8926
|
+
return err(
|
|
8927
|
+
HaiAIError.RESOURCE_OPERATION_FAILED,
|
|
8928
|
+
aiM("ai_resourceOperationFailed")
|
|
8929
|
+
);
|
|
8930
|
+
}
|
|
6126
8931
|
if (result.success) {
|
|
6127
8932
|
logger18.info("LLM chat completed", {
|
|
6128
8933
|
...context,
|
|
@@ -6155,7 +8960,11 @@ function createAILLMFunctions(config, deps) {
|
|
|
6155
8960
|
content: choice?.message?.content ?? "",
|
|
6156
8961
|
toolCalls: choice?.message?.tool_calls,
|
|
6157
8962
|
finishReason: choice?.finish_reason ?? "stop",
|
|
6158
|
-
usage: response.usage ?? {
|
|
8963
|
+
usage: response.usage ?? {
|
|
8964
|
+
prompt_tokens: 0,
|
|
8965
|
+
completion_tokens: 0,
|
|
8966
|
+
total_tokens: 0
|
|
8967
|
+
}
|
|
6159
8968
|
},
|
|
6160
8969
|
createdAt: Date.now(),
|
|
6161
8970
|
duration: Date.now() - start
|
|
@@ -6168,8 +8977,13 @@ function createAILLMFunctions(config, deps) {
|
|
|
6168
8977
|
return result;
|
|
6169
8978
|
}
|
|
6170
8979
|
async function* chatStreamWithRecord(request) {
|
|
8980
|
+
if (request.tempModel && Object.hasOwn(request.tempModel, "api"))
|
|
8981
|
+
throw new Error(aiM("ai_llmProtocolRenamed"));
|
|
6171
8982
|
const start = Date.now();
|
|
6172
|
-
const logContext = {
|
|
8983
|
+
const logContext = {
|
|
8984
|
+
model: request.model ?? request.tempModel?.model,
|
|
8985
|
+
messageCount: request.messages.length
|
|
8986
|
+
};
|
|
6173
8987
|
logger18.debug("LLM chat stream started", logContext);
|
|
6174
8988
|
const shouldRecord = !!(request.objectId && recordStore && request.enablePersist !== false);
|
|
6175
8989
|
let content = "";
|
|
@@ -6178,7 +8992,7 @@ function createAILLMFunctions(config, deps) {
|
|
|
6178
8992
|
let streamId = "";
|
|
6179
8993
|
let resolvedModel = request.model ?? request.tempModel?.model ?? "";
|
|
6180
8994
|
try {
|
|
6181
|
-
for await (const chunk of
|
|
8995
|
+
for await (const chunk of (await currentProvider()).chatStream(request)) {
|
|
6182
8996
|
yield chunk;
|
|
6183
8997
|
if (!shouldRecord)
|
|
6184
8998
|
continue;
|
|
@@ -6194,7 +9008,11 @@ function createAILLMFunctions(config, deps) {
|
|
|
6194
9008
|
if (choice.delta.tool_calls) {
|
|
6195
9009
|
for (const tc of choice.delta.tool_calls) {
|
|
6196
9010
|
if (!toolCalls[tc.index]) {
|
|
6197
|
-
toolCalls[tc.index] = {
|
|
9011
|
+
toolCalls[tc.index] = {
|
|
9012
|
+
id: tc.id ?? "",
|
|
9013
|
+
type: "function",
|
|
9014
|
+
function: { name: tc.function?.name ?? "", arguments: "" }
|
|
9015
|
+
};
|
|
6198
9016
|
}
|
|
6199
9017
|
if (tc.function?.arguments)
|
|
6200
9018
|
toolCalls[tc.index].function.arguments += tc.function.arguments;
|
|
@@ -6214,12 +9032,20 @@ function createAILLMFunctions(config, deps) {
|
|
|
6214
9032
|
finishReason
|
|
6215
9033
|
});
|
|
6216
9034
|
} catch (error) {
|
|
6217
|
-
logger18.warn("LLM chat stream failed", {
|
|
9035
|
+
logger18.warn("LLM chat stream failed", {
|
|
9036
|
+
...logContext,
|
|
9037
|
+
durationMs: Date.now() - start,
|
|
9038
|
+
error
|
|
9039
|
+
});
|
|
6218
9040
|
throw error;
|
|
6219
9041
|
}
|
|
6220
9042
|
if (shouldRecord) {
|
|
6221
9043
|
try {
|
|
6222
|
-
const usage = {
|
|
9044
|
+
const usage = {
|
|
9045
|
+
prompt_tokens: 0,
|
|
9046
|
+
completion_tokens: 0,
|
|
9047
|
+
total_tokens: 0
|
|
9048
|
+
};
|
|
6223
9049
|
const record = {
|
|
6224
9050
|
id: `${streamId || "stream"}_${Date.now()}_${recordSeq++}`,
|
|
6225
9051
|
objectId: request.objectId,
|
|
@@ -6256,7 +9082,15 @@ function createAILLMFunctions(config, deps) {
|
|
|
6256
9082
|
});
|
|
6257
9083
|
return ok(records);
|
|
6258
9084
|
} catch (error) {
|
|
6259
|
-
return err(
|
|
9085
|
+
return err(
|
|
9086
|
+
HaiAIError.LLM_HISTORY_FAILED,
|
|
9087
|
+
aiM("ai_llmHistoryFailed", {
|
|
9088
|
+
params: {
|
|
9089
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9090
|
+
}
|
|
9091
|
+
}),
|
|
9092
|
+
error
|
|
9093
|
+
);
|
|
6260
9094
|
}
|
|
6261
9095
|
}
|
|
6262
9096
|
async function listSessions(objectId) {
|
|
@@ -6270,13 +9104,30 @@ function createAILLMFunctions(config, deps) {
|
|
|
6270
9104
|
});
|
|
6271
9105
|
return ok(sessions.filter((item) => !item.deleted));
|
|
6272
9106
|
} catch (error) {
|
|
6273
|
-
return err(
|
|
9107
|
+
return err(
|
|
9108
|
+
HaiAIError.LLM_HISTORY_FAILED,
|
|
9109
|
+
aiM("ai_llmHistoryFailed", {
|
|
9110
|
+
params: {
|
|
9111
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9112
|
+
}
|
|
9113
|
+
}),
|
|
9114
|
+
error
|
|
9115
|
+
);
|
|
6274
9116
|
}
|
|
6275
9117
|
}
|
|
6276
9118
|
const llm = {
|
|
6277
9119
|
chat: chatWithRecord,
|
|
6278
9120
|
chatStream: chatStreamWithRecord,
|
|
6279
|
-
listModels: () =>
|
|
9121
|
+
listModels: async () => {
|
|
9122
|
+
try {
|
|
9123
|
+
return await (await currentProvider()).listModels();
|
|
9124
|
+
} catch {
|
|
9125
|
+
return err(
|
|
9126
|
+
HaiAIError.RESOURCE_OPERATION_FAILED,
|
|
9127
|
+
aiM("ai_resourceOperationFailed")
|
|
9128
|
+
);
|
|
9129
|
+
}
|
|
9130
|
+
},
|
|
6280
9131
|
getHistory,
|
|
6281
9132
|
listSessions,
|
|
6282
9133
|
async ask(question, options) {
|
|
@@ -6330,7 +9181,15 @@ function createAILLMFunctions(config, deps) {
|
|
|
6330
9181
|
const maxRepairs = request.maxRepairs ?? 1;
|
|
6331
9182
|
let lastError = "";
|
|
6332
9183
|
for (let attempt = 0; attempt <= maxRepairs; attempt++) {
|
|
6333
|
-
const messages = attempt === 0 ? baseMessages : [
|
|
9184
|
+
const messages = attempt === 0 ? baseMessages : [
|
|
9185
|
+
...baseMessages,
|
|
9186
|
+
{
|
|
9187
|
+
role: "user",
|
|
9188
|
+
content: aiM("ai_generateObjectRepair", {
|
|
9189
|
+
params: { error: lastError }
|
|
9190
|
+
})
|
|
9191
|
+
}
|
|
9192
|
+
];
|
|
6334
9193
|
const result = await chatWithRecord({
|
|
6335
9194
|
messages,
|
|
6336
9195
|
model: request.model,
|
|
@@ -6338,7 +9197,10 @@ function createAILLMFunctions(config, deps) {
|
|
|
6338
9197
|
tempModel: request.tempModel,
|
|
6339
9198
|
enablePersist: false,
|
|
6340
9199
|
signal: request.signal,
|
|
6341
|
-
response_format: {
|
|
9200
|
+
response_format: {
|
|
9201
|
+
type: "json_schema",
|
|
9202
|
+
json_schema: { name: schemaName, schema: jsonSchema, strict: true }
|
|
9203
|
+
}
|
|
6342
9204
|
});
|
|
6343
9205
|
if (!result.success)
|
|
6344
9206
|
return result;
|
|
@@ -6356,7 +9218,12 @@ function createAILLMFunctions(config, deps) {
|
|
|
6356
9218
|
return ok(validated.data);
|
|
6357
9219
|
lastError = validated.error.message;
|
|
6358
9220
|
}
|
|
6359
|
-
return err(
|
|
9221
|
+
return err(
|
|
9222
|
+
HaiAIError.INVALID_REQUEST,
|
|
9223
|
+
aiM("ai_generateObjectFailed", {
|
|
9224
|
+
params: { error: lastError || "unknown" }
|
|
9225
|
+
})
|
|
9226
|
+
);
|
|
6360
9227
|
}
|
|
6361
9228
|
};
|
|
6362
9229
|
const tools = {
|
|
@@ -6494,25 +9361,25 @@ function createAIMCPFunctions() {
|
|
|
6494
9361
|
}
|
|
6495
9362
|
};
|
|
6496
9363
|
}
|
|
6497
|
-
function createScopedMemory(
|
|
9364
|
+
function createScopedMemory(core32, binding) {
|
|
6498
9365
|
const { objectId, scope } = binding;
|
|
6499
9366
|
const accessScope = { objectId, scope };
|
|
6500
9367
|
return {
|
|
6501
|
-
extract: (messages, options) =>
|
|
6502
|
-
recall: (query, options) =>
|
|
6503
|
-
injectMemories: (messages, options) =>
|
|
6504
|
-
add: (entry) =>
|
|
6505
|
-
update: (memoryId, updates) =>
|
|
6506
|
-
get: (memoryId) =>
|
|
6507
|
-
remove: (memoryId) =>
|
|
6508
|
-
list: (options) =>
|
|
6509
|
-
listPage: (options) =>
|
|
6510
|
-
clear: (options) =>
|
|
9368
|
+
extract: (messages, options) => core32.extract(messages, { ...options, objectId, scope }),
|
|
9369
|
+
recall: (query, options) => core32.recall(query, { ...options, objectId, scope }),
|
|
9370
|
+
injectMemories: (messages, options) => core32.injectMemories(messages, { ...options, objectId, scope }),
|
|
9371
|
+
add: (entry) => core32.add({ ...entry, objectId, scope }),
|
|
9372
|
+
update: (memoryId, updates) => core32.update(memoryId, updates, accessScope),
|
|
9373
|
+
get: (memoryId) => core32.get(memoryId, accessScope),
|
|
9374
|
+
remove: (memoryId) => core32.remove(memoryId, accessScope),
|
|
9375
|
+
list: (options) => core32.list({ ...options, objectId, scope }),
|
|
9376
|
+
listPage: (options) => core32.listPage({ ...options, objectId, scope }),
|
|
9377
|
+
clear: (options) => core32.clear({ ...options, objectId, scope })
|
|
6511
9378
|
};
|
|
6512
9379
|
}
|
|
6513
|
-
function createMemoryOperations(
|
|
9380
|
+
function createMemoryOperations(core32) {
|
|
6514
9381
|
return {
|
|
6515
|
-
...
|
|
9382
|
+
...core32,
|
|
6516
9383
|
/**
|
|
6517
9384
|
* 清空记忆(空过滤保护)。
|
|
6518
9385
|
*
|
|
@@ -6524,15 +9391,15 @@ function createMemoryOperations(core33) {
|
|
|
6524
9391
|
if (!hasFilter) {
|
|
6525
9392
|
return err(HaiAIError.MEMORY_STORE_FAILED, aiM("ai_memoryClearRequiresFilter"));
|
|
6526
9393
|
}
|
|
6527
|
-
return
|
|
9394
|
+
return core32.clear(options);
|
|
6528
9395
|
},
|
|
6529
|
-
scoped: (binding) => createScopedMemory(
|
|
9396
|
+
scoped: (binding) => createScopedMemory(core32, binding),
|
|
6530
9397
|
admin: {
|
|
6531
9398
|
async clearAll(options) {
|
|
6532
9399
|
if (options?.confirm !== true) {
|
|
6533
9400
|
return err(HaiAIError.MEMORY_STORE_FAILED, aiM("ai_memoryClearAllUnconfirmed"));
|
|
6534
9401
|
}
|
|
6535
|
-
return
|
|
9402
|
+
return core32.clear();
|
|
6536
9403
|
}
|
|
6537
9404
|
}
|
|
6538
9405
|
};
|
|
@@ -6883,7 +9750,7 @@ function buildMem0Config(deps) {
|
|
|
6883
9750
|
const vectorResolved = buildVectorStoreConfig(deps);
|
|
6884
9751
|
if (!vectorResolved.success)
|
|
6885
9752
|
return vectorResolved;
|
|
6886
|
-
if (core.module.requiresSharedState(
|
|
9753
|
+
if (core.module.requiresSharedState(process2.env) && !vectorResolved.data.persistent)
|
|
6887
9754
|
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_memorySharedBackendRequired"));
|
|
6888
9755
|
return ok({
|
|
6889
9756
|
vectorProvider: vectorResolved.data.provider,
|
|
@@ -7095,16 +9962,16 @@ async function createMem0OssMemoryOperations(deps) {
|
|
|
7095
9962
|
const sdk = await import('@qdrant/js-client-rest').catch(() => void 0);
|
|
7096
9963
|
if (sdk && typeof sdk.QdrantClient?.prototype?.search === "function") {
|
|
7097
9964
|
const url = deps.vectorBackend?.url ?? "";
|
|
7098
|
-
const
|
|
9965
|
+
const client2 = new sdk.QdrantClient({
|
|
7099
9966
|
url,
|
|
7100
9967
|
port: Number(new URL(url).port) || 6333,
|
|
7101
9968
|
apiKey: deps.vectorBackend?.apiKey
|
|
7102
9969
|
});
|
|
7103
|
-
const upsert =
|
|
7104
|
-
const remove =
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
configResult.data.config.vectorStore.config.client =
|
|
9970
|
+
const upsert = client2.upsert.bind(client2);
|
|
9971
|
+
const remove = client2.delete.bind(client2);
|
|
9972
|
+
client2.upsert = (collection, options) => upsert(collection, { ...options, wait: true });
|
|
9973
|
+
client2.delete = (collection, options) => remove(collection, { ...options, wait: true });
|
|
9974
|
+
configResult.data.config.vectorStore.config.client = client2;
|
|
7108
9975
|
} else {
|
|
7109
9976
|
const failure = err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_memoryBackendSdkIncompatible"));
|
|
7110
9977
|
if (!failure.success)
|
|
@@ -8397,7 +11264,8 @@ function createRerankOperations(config) {
|
|
|
8397
11264
|
"Content-Type": "application/json",
|
|
8398
11265
|
"Authorization": `Bearer ${resolved.apiKey}`
|
|
8399
11266
|
},
|
|
8400
|
-
body: JSON.stringify(body)
|
|
11267
|
+
body: JSON.stringify(body),
|
|
11268
|
+
signal: AbortSignal.timeout(resolved.timeout)
|
|
8401
11269
|
});
|
|
8402
11270
|
if (!response.ok) {
|
|
8403
11271
|
const errorText = await response.text().catch(() => response.statusText);
|
|
@@ -8447,7 +11315,7 @@ function createRerankOperations(config) {
|
|
|
8447
11315
|
};
|
|
8448
11316
|
}
|
|
8449
11317
|
var logger28 = core.logger.child({ module: "ai", scope: "retrieval" });
|
|
8450
|
-
function createRetrievalOperations(embeddingOps, storeProvider, sourceStore, rerankOps) {
|
|
11318
|
+
function createRetrievalOperations(embeddingOps, storeProvider, sourceStore, rerankOps, registry) {
|
|
8451
11319
|
const vectorStoreCache = /* @__PURE__ */ new Map();
|
|
8452
11320
|
function getVectorStore(collection) {
|
|
8453
11321
|
let store = vectorStoreCache.get(collection);
|
|
@@ -8468,6 +11336,10 @@ function createRetrievalOperations(embeddingOps, storeProvider, sourceStore, rer
|
|
|
8468
11336
|
* @returns `ok(undefined)` 注册成功;`err` 含错误码 `RETRIEVAL_FAILED`(重复注册)
|
|
8469
11337
|
*/
|
|
8470
11338
|
async addSource(source) {
|
|
11339
|
+
if (registry) {
|
|
11340
|
+
const result = await registry.operations.register(source);
|
|
11341
|
+
return result.success ? ok(void 0) : result;
|
|
11342
|
+
}
|
|
8471
11343
|
const existing = await sourceStore.get(source.id);
|
|
8472
11344
|
if (existing) {
|
|
8473
11345
|
return err(HaiAIError.RETRIEVAL_FAILED, aiM("ai_internalError", { params: { error: `Source '${source.id}' already exists` } }));
|
|
@@ -8485,6 +11357,8 @@ function createRetrievalOperations(embeddingOps, storeProvider, sourceStore, rer
|
|
|
8485
11357
|
* @returns `ok(undefined)` 删除成功;`err` 含错误码 `RETRIEVAL_SOURCE_NOT_FOUND`
|
|
8486
11358
|
*/
|
|
8487
11359
|
async removeSource(sourceId) {
|
|
11360
|
+
if (registry)
|
|
11361
|
+
return registry.operations.remove(sourceId);
|
|
8488
11362
|
const existing = await sourceStore.get(sourceId);
|
|
8489
11363
|
if (!existing) {
|
|
8490
11364
|
return err(HaiAIError.RETRIEVAL_SOURCE_NOT_FOUND, aiM("ai_internalError", { params: { error: `Source '${sourceId}' not found` } }));
|
|
@@ -8499,7 +11373,7 @@ function createRetrievalOperations(embeddingOps, storeProvider, sourceStore, rer
|
|
|
8499
11373
|
* @returns 全部检索源数组(无源时返回空数组)
|
|
8500
11374
|
*/
|
|
8501
11375
|
async listSources() {
|
|
8502
|
-
return sourceStore.query({});
|
|
11376
|
+
return registry ? registry.values() : sourceStore.query({});
|
|
8503
11377
|
},
|
|
8504
11378
|
/**
|
|
8505
11379
|
* 执行向量检索
|
|
@@ -8514,13 +11388,13 @@ function createRetrievalOperations(embeddingOps, storeProvider, sourceStore, rer
|
|
|
8514
11388
|
*/
|
|
8515
11389
|
async retrieve(request) {
|
|
8516
11390
|
const startTime = Date.now();
|
|
8517
|
-
const targetSources = await sourceStore.query(
|
|
8518
|
-
request.sources?.length ? { where: { id: { $in: request.sources } } } : {}
|
|
8519
|
-
);
|
|
8520
|
-
if (targetSources.length === 0) {
|
|
8521
|
-
return err(HaiAIError.RETRIEVAL_SOURCE_NOT_FOUND, aiM("ai_internalError", { params: { error: "No retrieval sources configured" } }));
|
|
8522
|
-
}
|
|
8523
11391
|
try {
|
|
11392
|
+
const targetSources = registry ? (await registry.values()).filter((source) => !request.sources?.length || request.sources.includes(source.id)) : await sourceStore.query(
|
|
11393
|
+
request.sources?.length ? { where: { id: { $in: request.sources } } } : {}
|
|
11394
|
+
);
|
|
11395
|
+
if (targetSources.length === 0) {
|
|
11396
|
+
return err(HaiAIError.RETRIEVAL_SOURCE_NOT_FOUND, aiM("ai_internalError", { params: { error: "No retrieval sources configured" } }));
|
|
11397
|
+
}
|
|
8524
11398
|
const embedResult = await embeddingOps.embedText(request.query);
|
|
8525
11399
|
if (!embedResult.success) {
|
|
8526
11400
|
return err(HaiAIError.RETRIEVAL_FAILED, aiM("ai_internalError", { params: { error: "Failed to embed query" } }), embedResult.error);
|
|
@@ -8699,38 +11573,207 @@ function createTokenOperations(config) {
|
|
|
8699
11573
|
}
|
|
8700
11574
|
|
|
8701
11575
|
// src/ai-functions.ts
|
|
8702
|
-
var logger30 = core.logger.child({ module: "ai", scope: "functions" });
|
|
8703
11576
|
async function createAISubsystems(config, deps) {
|
|
8704
11577
|
const { storeProvider, datapipe: datapipeDep } = deps;
|
|
8705
11578
|
const memoryParsed = MemoryConfigSchema.parse(config.memory ?? {});
|
|
8706
11579
|
const useMem0Oss = memoryParsed.provider === "mem0";
|
|
8707
|
-
const chatRecordStore = storeProvider.createRelStore(
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
const
|
|
8712
|
-
|
|
11580
|
+
const chatRecordStore = storeProvider.createRelStore(
|
|
11581
|
+
"hai_ai_chat_records",
|
|
11582
|
+
{ hasObjectId: true, hasSessionId: true }
|
|
11583
|
+
);
|
|
11584
|
+
const sessionStore = storeProvider.createRelStore(
|
|
11585
|
+
"hai_ai_sessions",
|
|
11586
|
+
{ hasObjectId: true }
|
|
11587
|
+
);
|
|
11588
|
+
const sourceStore = storeProvider.createRelStore(
|
|
11589
|
+
"hai_ai_retrieval_sources"
|
|
11590
|
+
);
|
|
11591
|
+
const memoryStore = useMem0Oss ? null : storeProvider.createRelStore("hai_ai_memory", {
|
|
11592
|
+
hasObjectId: true,
|
|
11593
|
+
hasScopeIndex: true
|
|
11594
|
+
});
|
|
11595
|
+
const contextStore = storeProvider.createRelStore(
|
|
11596
|
+
"hai_ai_context",
|
|
11597
|
+
{ hasObjectId: true, hasSessionId: true }
|
|
11598
|
+
);
|
|
11599
|
+
const personaStore = storeProvider.createRelStore(
|
|
11600
|
+
"hai_ai_persona",
|
|
11601
|
+
{ hasObjectId: true }
|
|
11602
|
+
);
|
|
8713
11603
|
const a2aStores = config.a2a ? createA2AStores(storeProvider) : null;
|
|
8714
11604
|
const memoryVectorStore = useMem0Oss ? null : storeProvider.createVectorStore("hai_ai_memory_vectors");
|
|
11605
|
+
let registriesOpen = true;
|
|
11606
|
+
const isOpen = () => registriesOpen;
|
|
11607
|
+
const llmModels = createResourceRegistry(
|
|
11608
|
+
storeProvider.createRelStore("hai_ai_llm_models"),
|
|
11609
|
+
ModelEntrySchema,
|
|
11610
|
+
config.llm.models ?? [],
|
|
11611
|
+
isOpen
|
|
11612
|
+
);
|
|
11613
|
+
const audioModels = createResourceRegistry(
|
|
11614
|
+
storeProvider.createRelStore("hai_ai_audio_models"),
|
|
11615
|
+
AudioModelEntrySchema,
|
|
11616
|
+
config.audio?.models ?? [],
|
|
11617
|
+
isOpen
|
|
11618
|
+
);
|
|
11619
|
+
const imageModels = createResourceRegistry(
|
|
11620
|
+
storeProvider.createRelStore("hai_ai_image_models"),
|
|
11621
|
+
ImageModelEntrySchema,
|
|
11622
|
+
config.image?.models ?? [],
|
|
11623
|
+
isOpen
|
|
11624
|
+
);
|
|
11625
|
+
const mcpServers = createResourceRegistry(
|
|
11626
|
+
storeProvider.createRelStore("hai_ai_mcp_servers"),
|
|
11627
|
+
MCPExternalServerSchema,
|
|
11628
|
+
config.mcp?.servers ?? [],
|
|
11629
|
+
isOpen
|
|
11630
|
+
);
|
|
11631
|
+
const retrievalSources = createResourceRegistry(
|
|
11632
|
+
sourceStore,
|
|
11633
|
+
RetrievalSourceSchema,
|
|
11634
|
+
config.retrieval?.sources ?? [],
|
|
11635
|
+
isOpen
|
|
11636
|
+
);
|
|
11637
|
+
async function llmConfig() {
|
|
11638
|
+
return {
|
|
11639
|
+
...config,
|
|
11640
|
+
llm: { ...config.llm, models: await llmModels.values() }
|
|
11641
|
+
};
|
|
11642
|
+
}
|
|
11643
|
+
async function audioConfig() {
|
|
11644
|
+
return {
|
|
11645
|
+
...config,
|
|
11646
|
+
audio: AudioConfigSchema.parse({
|
|
11647
|
+
...config.audio,
|
|
11648
|
+
models: await audioModels.values()
|
|
11649
|
+
})
|
|
11650
|
+
};
|
|
11651
|
+
}
|
|
11652
|
+
async function imageConfig() {
|
|
11653
|
+
return {
|
|
11654
|
+
...config,
|
|
11655
|
+
image: ImageConfigSchema.parse({
|
|
11656
|
+
...config.image,
|
|
11657
|
+
models: await imageModels.values()
|
|
11658
|
+
})
|
|
11659
|
+
};
|
|
11660
|
+
}
|
|
11661
|
+
async function mcpLookup(ids) {
|
|
11662
|
+
const servers = [];
|
|
11663
|
+
for (const id of ids) {
|
|
11664
|
+
const entry = await mcpServers.read(id);
|
|
11665
|
+
if (!entry)
|
|
11666
|
+
throw new Error("MCP resource missing");
|
|
11667
|
+
const server = entry.value;
|
|
11668
|
+
if (server.transport === "stdio") {
|
|
11669
|
+
servers.push({
|
|
11670
|
+
name: server.name,
|
|
11671
|
+
command: server.command,
|
|
11672
|
+
args: server.args,
|
|
11673
|
+
env: Object.entries(resolveEnvironment(server.environmentRefs)).map(
|
|
11674
|
+
([name, value]) => ({ name, value })
|
|
11675
|
+
)
|
|
11676
|
+
});
|
|
11677
|
+
} else {
|
|
11678
|
+
servers.push({
|
|
11679
|
+
type: server.transport,
|
|
11680
|
+
name: server.name,
|
|
11681
|
+
url: server.url,
|
|
11682
|
+
headers: Object.entries(resolveEnvironment(server.headerEnv)).map(
|
|
11683
|
+
([name, value]) => ({ name, value })
|
|
11684
|
+
)
|
|
11685
|
+
});
|
|
11686
|
+
}
|
|
11687
|
+
}
|
|
11688
|
+
return servers;
|
|
11689
|
+
}
|
|
11690
|
+
const acp = createACPOperations(
|
|
11691
|
+
storeProvider,
|
|
11692
|
+
config,
|
|
11693
|
+
deps.acpAdapters,
|
|
11694
|
+
llmConfig,
|
|
11695
|
+
mcpLookup
|
|
11696
|
+
);
|
|
8715
11697
|
await storeProvider.initialize();
|
|
8716
|
-
const llmFunctions = createAILLMFunctions(config, {
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
const
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
11698
|
+
const llmFunctions = createAILLMFunctions(config, {
|
|
11699
|
+
recordStore: chatRecordStore,
|
|
11700
|
+
sessionStore,
|
|
11701
|
+
resolveConfig: llmConfig
|
|
11702
|
+
});
|
|
11703
|
+
const llm = Object.assign(llmFunctions.llm, {
|
|
11704
|
+
models: llmModels.operations
|
|
11705
|
+
});
|
|
11706
|
+
const mcp = Object.assign(createAIMCPFunctions(), {
|
|
11707
|
+
servers: mcpServers.operations
|
|
11708
|
+
});
|
|
11709
|
+
async function resourceCall(operation) {
|
|
11710
|
+
try {
|
|
11711
|
+
return await operation();
|
|
11712
|
+
} catch {
|
|
11713
|
+
return err(
|
|
11714
|
+
HaiAIError.RESOURCE_OPERATION_FAILED,
|
|
11715
|
+
aiM("ai_resourceOperationFailed")
|
|
11716
|
+
);
|
|
11717
|
+
}
|
|
8726
11718
|
}
|
|
11719
|
+
const embedding = {
|
|
11720
|
+
embed: (request) => resourceCall(
|
|
11721
|
+
async () => createEmbeddingOperations(await llmConfig()).embed(request)
|
|
11722
|
+
),
|
|
11723
|
+
embedText: (text) => resourceCall(
|
|
11724
|
+
async () => createEmbeddingOperations(await llmConfig()).embedText(text)
|
|
11725
|
+
),
|
|
11726
|
+
embedBatch: (texts) => resourceCall(
|
|
11727
|
+
async () => createEmbeddingOperations(await llmConfig()).embedBatch(texts)
|
|
11728
|
+
)
|
|
11729
|
+
};
|
|
11730
|
+
const reasoning = {
|
|
11731
|
+
run: (query, options) => resourceCall(async () => createReasoningOperations(await llmConfig(), llm).run(query, options)),
|
|
11732
|
+
async *runStream(query, options) {
|
|
11733
|
+
yield* createReasoningOperations(await llmConfig(), llm).runStream(query, options);
|
|
11734
|
+
}
|
|
11735
|
+
};
|
|
11736
|
+
const rerank = {
|
|
11737
|
+
rerank: (request) => resourceCall(
|
|
11738
|
+
async () => createRerankOperations(await llmConfig()).rerank(request)
|
|
11739
|
+
),
|
|
11740
|
+
rerankTexts: (query, texts, topN) => resourceCall(
|
|
11741
|
+
async () => createRerankOperations(await llmConfig()).rerankTexts(
|
|
11742
|
+
query,
|
|
11743
|
+
texts,
|
|
11744
|
+
topN
|
|
11745
|
+
)
|
|
11746
|
+
)
|
|
11747
|
+
};
|
|
11748
|
+
const retrieval = Object.assign(
|
|
11749
|
+
createRetrievalOperations(
|
|
11750
|
+
embedding,
|
|
11751
|
+
storeProvider,
|
|
11752
|
+
sourceStore,
|
|
11753
|
+
rerank,
|
|
11754
|
+
retrievalSources
|
|
11755
|
+
),
|
|
11756
|
+
{ sources: retrievalSources.operations }
|
|
11757
|
+
);
|
|
8727
11758
|
const rag = createRagOperations(llm, retrieval);
|
|
8728
11759
|
const knowledgeParsed = KnowledgeConfigSchema.parse(config.knowledge ?? {});
|
|
8729
11760
|
const knowledgeStore = storeProvider.createKnowledgeStore?.();
|
|
8730
|
-
const knowledge = createKnowledgeOperations(
|
|
8731
|
-
|
|
11761
|
+
const knowledge = createKnowledgeOperations(
|
|
11762
|
+
knowledgeParsed,
|
|
11763
|
+
llm,
|
|
11764
|
+
embedding,
|
|
11765
|
+
datapipeDep,
|
|
11766
|
+
knowledgeStore
|
|
11767
|
+
);
|
|
11768
|
+
const memoryCore = memoryStore && memoryVectorStore ? createNativeMemoryOperations(
|
|
11769
|
+
memoryParsed,
|
|
11770
|
+
llm,
|
|
11771
|
+
embedding,
|
|
11772
|
+
memoryStore,
|
|
11773
|
+
memoryVectorStore
|
|
11774
|
+
) : await createMem0OssMemoryOperations({
|
|
8732
11775
|
config: memoryParsed,
|
|
8733
|
-
aiConfig:
|
|
11776
|
+
aiConfig: await llmConfig(),
|
|
8734
11777
|
llm,
|
|
8735
11778
|
collectionName: "hai_ai_memory",
|
|
8736
11779
|
embeddingDims: config.embedding?.dimensions,
|
|
@@ -8741,20 +11784,109 @@ async function createAISubsystems(config, deps) {
|
|
|
8741
11784
|
const summaryParsed = SummaryConfigSchema.parse(config.summary ?? {});
|
|
8742
11785
|
const compressParsed = CompressConfigSchema.parse(config.compress ?? {});
|
|
8743
11786
|
const token = createTokenOperations(tokenParsed);
|
|
8744
|
-
const summary =
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
summary
|
|
8752
|
-
|
|
11787
|
+
const summary = {
|
|
11788
|
+
generate: (messages, options) => resourceCall(async () => createSummaryOperations((await llmConfig()).llm, llm, token, summaryParsed).generate(messages, options)),
|
|
11789
|
+
summarize: (messages, options) => resourceCall(async () => createSummaryOperations((await llmConfig()).llm, llm, token, summaryParsed).summarize(messages, options))
|
|
11790
|
+
};
|
|
11791
|
+
const compress = createCompressOperations(
|
|
11792
|
+
compressParsed,
|
|
11793
|
+
token,
|
|
11794
|
+
summary,
|
|
11795
|
+
config.llm.maxTokens ?? 4096
|
|
11796
|
+
);
|
|
11797
|
+
const context = createContextOperations(
|
|
11798
|
+
compressParsed,
|
|
11799
|
+
token,
|
|
11800
|
+
compress,
|
|
11801
|
+
contextStore,
|
|
11802
|
+
sessionStore,
|
|
11803
|
+
{
|
|
11804
|
+
llm,
|
|
11805
|
+
memory: memory ?? void 0,
|
|
11806
|
+
rag: rag ?? void 0,
|
|
11807
|
+
reasoning: reasoning ?? void 0,
|
|
11808
|
+
summary
|
|
11809
|
+
}
|
|
11810
|
+
);
|
|
8753
11811
|
const persona = createPersonaOperations(personaStore);
|
|
8754
|
-
const file =
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
11812
|
+
const file = {
|
|
11813
|
+
parse: (request) => resourceCall(async () => createFileOperations(await llmConfig(), llm).parse(request)),
|
|
11814
|
+
parseText: (content, filename) => resourceCall(async () => createFileOperations(await llmConfig(), llm).parseText(content, filename))
|
|
11815
|
+
};
|
|
11816
|
+
let audioSnapshot = createAudioOperations(await audioConfig());
|
|
11817
|
+
async function currentAudio2() {
|
|
11818
|
+
audioSnapshot = createAudioOperations(await audioConfig());
|
|
11819
|
+
return audioSnapshot;
|
|
11820
|
+
}
|
|
11821
|
+
const audio = {
|
|
11822
|
+
models: {
|
|
11823
|
+
list: () => resourceCall(async () => {
|
|
11824
|
+
await currentAudio2();
|
|
11825
|
+
return audioModels.operations.list();
|
|
11826
|
+
}),
|
|
11827
|
+
get: (id) => resourceCall(async () => {
|
|
11828
|
+
await currentAudio2();
|
|
11829
|
+
return audioModels.operations.get(id);
|
|
11830
|
+
}),
|
|
11831
|
+
register: (input) => resourceCall(async () => {
|
|
11832
|
+
const result = await audioModels.operations.register(input);
|
|
11833
|
+
if (result.success)
|
|
11834
|
+
await currentAudio2();
|
|
11835
|
+
return result;
|
|
11836
|
+
}),
|
|
11837
|
+
update: (input, version) => resourceCall(async () => {
|
|
11838
|
+
const result = await audioModels.operations.update(input, version);
|
|
11839
|
+
if (result.success)
|
|
11840
|
+
await currentAudio2();
|
|
11841
|
+
return result;
|
|
11842
|
+
}),
|
|
11843
|
+
remove: (id, version) => resourceCall(async () => {
|
|
11844
|
+
const result = await audioModels.operations.remove(id, version);
|
|
11845
|
+
if (result.success)
|
|
11846
|
+
await currentAudio2();
|
|
11847
|
+
return result;
|
|
11848
|
+
})
|
|
11849
|
+
},
|
|
11850
|
+
transcribe: (request) => resourceCall(async () => (await currentAudio2()).transcribe(request)),
|
|
11851
|
+
synthesize: (request) => resourceCall(async () => (await currentAudio2()).synthesize(request)),
|
|
11852
|
+
async *transcribeStream(request) {
|
|
11853
|
+
yield* (await currentAudio2()).transcribeStream(request);
|
|
11854
|
+
},
|
|
11855
|
+
async *synthesizeStream(request) {
|
|
11856
|
+
yield* (await currentAudio2()).synthesizeStream(request);
|
|
11857
|
+
},
|
|
11858
|
+
getCapabilities: (request) => audioSnapshot.getCapabilities(request)
|
|
11859
|
+
};
|
|
11860
|
+
const image = {
|
|
11861
|
+
models: imageModels.operations,
|
|
11862
|
+
generate: (request) => resourceCall(
|
|
11863
|
+
async () => createImageOperations(await imageConfig()).generate(request)
|
|
11864
|
+
)
|
|
11865
|
+
};
|
|
11866
|
+
return {
|
|
11867
|
+
closeRegistries: () => {
|
|
11868
|
+
registriesOpen = false;
|
|
11869
|
+
},
|
|
11870
|
+
a2aStores,
|
|
11871
|
+
llm,
|
|
11872
|
+
acp,
|
|
11873
|
+
mcp,
|
|
11874
|
+
embedding,
|
|
11875
|
+
reasoning,
|
|
11876
|
+
rerank,
|
|
11877
|
+
retrieval,
|
|
11878
|
+
rag,
|
|
11879
|
+
knowledge,
|
|
11880
|
+
memory,
|
|
11881
|
+
persona,
|
|
11882
|
+
token,
|
|
11883
|
+
summary,
|
|
11884
|
+
compress,
|
|
11885
|
+
context,
|
|
11886
|
+
file,
|
|
11887
|
+
audio,
|
|
11888
|
+
image
|
|
11889
|
+
};
|
|
8758
11890
|
}
|
|
8759
11891
|
var WRITE_LEASE_MS = 3e5;
|
|
8760
11892
|
var CAS_ATTEMPTS = 12;
|
|
@@ -9073,7 +12205,7 @@ function documentInfo(document) {
|
|
|
9073
12205
|
}
|
|
9074
12206
|
|
|
9075
12207
|
// src/store/providers/ai-store-provider-db.ts
|
|
9076
|
-
var
|
|
12208
|
+
var logger30 = core.logger.child({ module: "ai", scope: "store-provider-db" });
|
|
9077
12209
|
function requireSqlSuccess(result) {
|
|
9078
12210
|
if (!result.success)
|
|
9079
12211
|
throw new Error(result.error.message, { cause: result.error });
|
|
@@ -9592,7 +12724,7 @@ function createDbStoreProvider(deps) {
|
|
|
9592
12724
|
if (result.status === "rejected")
|
|
9593
12725
|
throw result.reason;
|
|
9594
12726
|
}
|
|
9595
|
-
|
|
12727
|
+
logger30.debug("DB store provider initialized", { tableCount: relStores.size });
|
|
9596
12728
|
}
|
|
9597
12729
|
},
|
|
9598
12730
|
async close() {
|
|
@@ -9823,10 +12955,11 @@ function createMemoryStoreProvider() {
|
|
|
9823
12955
|
}
|
|
9824
12956
|
|
|
9825
12957
|
// src/ai-main.ts
|
|
9826
|
-
var
|
|
12958
|
+
var logger31 = core.logger.child({ module: "ai", scope: "main" });
|
|
9827
12959
|
var initInProgress = false;
|
|
9828
12960
|
var currentConfig = null;
|
|
9829
12961
|
var currentLLM = null;
|
|
12962
|
+
var currentACP = null;
|
|
9830
12963
|
var currentMCP = null;
|
|
9831
12964
|
var currentEmbedding = null;
|
|
9832
12965
|
var currentReasoning = null;
|
|
@@ -9846,12 +12979,14 @@ var currentImage = null;
|
|
|
9846
12979
|
var currentA2AConfig;
|
|
9847
12980
|
var currentA2AImpl = null;
|
|
9848
12981
|
var currentA2AStores = null;
|
|
12982
|
+
var currentRegistriesClose = null;
|
|
9849
12983
|
var currentStoreProvider = null;
|
|
9850
12984
|
var notInitialized = core.module.createNotInitializedKit(
|
|
9851
12985
|
HaiAIError.NOT_INITIALIZED,
|
|
9852
12986
|
() => aiM("ai_notInitialized")
|
|
9853
12987
|
);
|
|
9854
12988
|
var notInitializedLLM = {
|
|
12989
|
+
models: notInitialized.proxy(),
|
|
9855
12990
|
chat: () => Promise.resolve(notInitialized.result()),
|
|
9856
12991
|
async *chatStream() {
|
|
9857
12992
|
throw notInitialized.error();
|
|
@@ -9865,7 +13000,16 @@ var notInitializedLLM = {
|
|
|
9865
13000
|
},
|
|
9866
13001
|
generateObject: () => Promise.resolve(notInitialized.result())
|
|
9867
13002
|
};
|
|
13003
|
+
var notInitializedACP = {
|
|
13004
|
+
agents: notInitialized.proxy(),
|
|
13005
|
+
sessions: notInitialized.proxy(),
|
|
13006
|
+
workspaces: notInitialized.proxy(),
|
|
13007
|
+
connect: () => Promise.resolve(notInitialized.result()),
|
|
13008
|
+
close: async () => {
|
|
13009
|
+
}
|
|
13010
|
+
};
|
|
9868
13011
|
var notInitializedMCP = {
|
|
13012
|
+
servers: notInitialized.proxy(),
|
|
9869
13013
|
registerTool: () => notInitialized.result(),
|
|
9870
13014
|
registerResource: () => notInitialized.result(),
|
|
9871
13015
|
registerPrompt: () => notInitialized.result(),
|
|
@@ -9881,6 +13025,7 @@ var notInitializedReasoning = {
|
|
|
9881
13025
|
}
|
|
9882
13026
|
};
|
|
9883
13027
|
var notInitializedRetrieval = {
|
|
13028
|
+
sources: notInitialized.proxy(),
|
|
9884
13029
|
addSource: () => Promise.resolve(notInitialized.result()),
|
|
9885
13030
|
removeSource: () => Promise.resolve(notInitialized.result()),
|
|
9886
13031
|
listSources: () => Promise.resolve([]),
|
|
@@ -9915,6 +13060,7 @@ var notInitializedContext = {
|
|
|
9915
13060
|
var notInitializedRerank = notInitialized.proxy();
|
|
9916
13061
|
var notInitializedFile = notInitialized.proxy();
|
|
9917
13062
|
var notInitializedAudio = {
|
|
13063
|
+
models: notInitialized.proxy(),
|
|
9918
13064
|
transcribe: () => Promise.resolve(notInitialized.result()),
|
|
9919
13065
|
async *transcribeStream() {
|
|
9920
13066
|
throw notInitialized.error();
|
|
@@ -9926,6 +13072,7 @@ var notInitializedAudio = {
|
|
|
9926
13072
|
getCapabilities: () => notInitialized.result()
|
|
9927
13073
|
};
|
|
9928
13074
|
var notInitializedImage = {
|
|
13075
|
+
models: notInitialized.proxy(),
|
|
9929
13076
|
generate: () => Promise.resolve(notInitialized.result())
|
|
9930
13077
|
};
|
|
9931
13078
|
var a2aLazyOperations = createA2ALazyProxy({
|
|
@@ -9941,7 +13088,11 @@ var a2aLazyOperations = createA2ALazyProxy({
|
|
|
9941
13088
|
async function resetAllState() {
|
|
9942
13089
|
const storeProvider = currentStoreProvider;
|
|
9943
13090
|
await currentA2AImpl?.close();
|
|
13091
|
+
await currentACP?.close();
|
|
9944
13092
|
currentLLM = null;
|
|
13093
|
+
currentRegistriesClose?.();
|
|
13094
|
+
currentRegistriesClose = null;
|
|
13095
|
+
currentACP = null;
|
|
9945
13096
|
currentMCP = null;
|
|
9946
13097
|
currentEmbedding = null;
|
|
9947
13098
|
currentReasoning = null;
|
|
@@ -9967,7 +13118,7 @@ async function resetAllState() {
|
|
|
9967
13118
|
try {
|
|
9968
13119
|
await storeProvider.close();
|
|
9969
13120
|
} catch (error) {
|
|
9970
|
-
|
|
13121
|
+
logger31.error("AI store provider close failed", { error });
|
|
9971
13122
|
}
|
|
9972
13123
|
}
|
|
9973
13124
|
}
|
|
@@ -9983,20 +13134,25 @@ var streamOperations = {
|
|
|
9983
13134
|
};
|
|
9984
13135
|
var ai = {
|
|
9985
13136
|
async init(config, options) {
|
|
13137
|
+
if (Object.keys(process2.env).some((name) => /^HAI_AI_LLM_(?:MODELS_\d+_)?API$/.test(name)))
|
|
13138
|
+
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_llmProtocolRenamed"));
|
|
9986
13139
|
if (initInProgress) {
|
|
9987
|
-
|
|
13140
|
+
logger31.warn("AI init already in progress, skipping concurrent call");
|
|
9988
13141
|
return err(HaiAIError.INIT_IN_PROGRESS, aiM("ai_initInProgress"));
|
|
9989
13142
|
}
|
|
9990
13143
|
initInProgress = true;
|
|
13144
|
+
let closingPrevious = false;
|
|
9991
13145
|
try {
|
|
9992
13146
|
if (currentConfig) {
|
|
9993
|
-
|
|
13147
|
+
logger31.warn("AI module is already initialized, reinitializing");
|
|
13148
|
+
closingPrevious = true;
|
|
9994
13149
|
await ai.close();
|
|
13150
|
+
closingPrevious = false;
|
|
9995
13151
|
}
|
|
9996
|
-
|
|
13152
|
+
logger31.info("Initializing AI module");
|
|
9997
13153
|
const parseResult = AIConfigSchema.safeParse(config ?? {});
|
|
9998
13154
|
if (!parseResult.success) {
|
|
9999
|
-
|
|
13155
|
+
logger31.error("AI config validation failed", { error: parseResult.error.message });
|
|
10000
13156
|
return err(
|
|
10001
13157
|
HaiAIError.CONFIGURATION_ERROR,
|
|
10002
13158
|
aiM("ai_configError", { params: { error: parseResult.error.message } }),
|
|
@@ -10011,16 +13167,19 @@ var ai = {
|
|
|
10011
13167
|
storeProvider = createDbStoreProviderFromModules();
|
|
10012
13168
|
} else {
|
|
10013
13169
|
storeProvider = createMemoryStoreProvider();
|
|
10014
|
-
|
|
13170
|
+
logger31.warn("AI is using the ephemeral in-memory store; initialize reldb and vecdb before ai.init() for persistent data");
|
|
10015
13171
|
}
|
|
10016
|
-
if (core.module.requiresSharedState(
|
|
13172
|
+
if (core.module.requiresSharedState(process2.env) && storeProvider.stateScope !== "shared")
|
|
10017
13173
|
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_sharedBackendRequired"));
|
|
10018
13174
|
currentStoreProvider = storeProvider;
|
|
10019
13175
|
const subs = await createAISubsystems(parsed, {
|
|
10020
13176
|
storeProvider,
|
|
10021
|
-
datapipe
|
|
13177
|
+
datapipe,
|
|
13178
|
+
acpAdapters: options?.acpAdapters
|
|
10022
13179
|
});
|
|
13180
|
+
currentRegistriesClose = subs.closeRegistries;
|
|
10023
13181
|
currentLLM = subs.llm;
|
|
13182
|
+
currentACP = subs.acp;
|
|
10024
13183
|
currentMCP = subs.mcp;
|
|
10025
13184
|
currentEmbedding = subs.embedding;
|
|
10026
13185
|
currentReasoning = subs.reasoning;
|
|
@@ -10040,11 +13199,17 @@ var ai = {
|
|
|
10040
13199
|
currentA2AConfig = parsed.a2a;
|
|
10041
13200
|
currentA2AStores = subs.a2aStores;
|
|
10042
13201
|
currentConfig = parsed;
|
|
10043
|
-
|
|
13202
|
+
logger31.info("AI module initialized", { model: parsed.llm?.model });
|
|
10044
13203
|
return ok(void 0);
|
|
10045
13204
|
} catch (error) {
|
|
10046
|
-
|
|
10047
|
-
|
|
13205
|
+
if (!closingPrevious) {
|
|
13206
|
+
try {
|
|
13207
|
+
await resetAllState();
|
|
13208
|
+
} catch (cleanupError) {
|
|
13209
|
+
logger31.error("AI initialization cleanup failed", { error: cleanupError });
|
|
13210
|
+
}
|
|
13211
|
+
}
|
|
13212
|
+
logger31.error("AI module initialization failed", { error });
|
|
10048
13213
|
return err(
|
|
10049
13214
|
HaiAIError.CONFIGURATION_ERROR,
|
|
10050
13215
|
aiM("ai_initFailed", {
|
|
@@ -10059,6 +13224,9 @@ var ai = {
|
|
|
10059
13224
|
get llm() {
|
|
10060
13225
|
return currentLLM ?? notInitializedLLM;
|
|
10061
13226
|
},
|
|
13227
|
+
get acp() {
|
|
13228
|
+
return currentACP ?? notInitializedACP;
|
|
13229
|
+
},
|
|
10062
13230
|
get mcp() {
|
|
10063
13231
|
return currentMCP ?? notInitializedMCP;
|
|
10064
13232
|
},
|
|
@@ -10124,12 +13292,12 @@ var ai = {
|
|
|
10124
13292
|
},
|
|
10125
13293
|
async close() {
|
|
10126
13294
|
if (!currentConfig) {
|
|
10127
|
-
|
|
13295
|
+
logger31.info("AI module already closed, skipping");
|
|
10128
13296
|
return;
|
|
10129
13297
|
}
|
|
10130
|
-
|
|
13298
|
+
logger31.info("Closing AI module");
|
|
10131
13299
|
await resetAllState();
|
|
10132
|
-
|
|
13300
|
+
logger31.info("AI module closed");
|
|
10133
13301
|
}
|
|
10134
13302
|
};
|
|
10135
13303
|
function createMcpServer(options) {
|