@h-ai/ai 1.0.0-beta.5 → 1.0.0-beta.7
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 +25 -0
- package/README.ja.md +25 -0
- package/README.md +25 -0
- package/dist/{ai-audio-ws-protocol-1VhScfis.d.ts → ai-acp-types-DQ8jO9Ie.d.ts} +2359 -201
- package/dist/browser.d.ts +4 -3
- package/dist/browser.js +3 -3
- package/dist/{chunk-GAC5RLAG.js → chunk-HEYVFQC6.js} +271 -7
- 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 +3308 -151
- 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-GAC5RLAG.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,2762 @@ 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
|
+
function powershell(script) {
|
|
1145
|
+
return ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")];
|
|
1146
|
+
}
|
|
1147
|
+
function windowsACPCommand(command, args) {
|
|
1148
|
+
const name = `Local\\hai-ai-acp-${randomUUID()}`;
|
|
1149
|
+
const key = `__HAI_AI_ACP_JOB_${randomUUID().replaceAll("-", "")}`;
|
|
1150
|
+
const script = `$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $InformationPreference='SilentlyContinue'
|
|
1151
|
+
try {
|
|
1152
|
+
Add-Type -TypeDefinition @'
|
|
1153
|
+
${nativeJob}
|
|
1154
|
+
'@
|
|
1155
|
+
$inputConfig = [Environment]::GetEnvironmentVariable('${key}') | ConvertFrom-Json
|
|
1156
|
+
[Environment]::SetEnvironmentVariable('${key}', $null)
|
|
1157
|
+
exit ([HaiACPCommandJob]::Run([string]$inputConfig.name, [string]$inputConfig.command, [string[]]$inputConfig.args))
|
|
1158
|
+
} catch { [Console]::Error.WriteLine('Windows ACP job failed'); exit 125 }`;
|
|
1159
|
+
return { name, args: powershell(script), env: { [key]: JSON.stringify({ name, command, args }) } };
|
|
1160
|
+
}
|
|
1161
|
+
function windowsACPJobProbe(name) {
|
|
1162
|
+
return powershell(`$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $InformationPreference='SilentlyContinue'
|
|
1163
|
+
try {
|
|
1164
|
+
Add-Type -TypeDefinition @'
|
|
1165
|
+
${nativeJob}
|
|
1166
|
+
'@
|
|
1167
|
+
if ([HaiACPCommandJob]::Empty('${name}')) { exit 0 } else { exit 1 }
|
|
1168
|
+
} catch { exit 2 }`);
|
|
1169
|
+
}
|
|
1170
|
+
async function jobProbe(name, timeout) {
|
|
1171
|
+
return new Promise((resolve3) => {
|
|
1172
|
+
const probe = spawn("powershell.exe", windowsACPJobProbe(name), { shell: false, windowsHide: true, stdio: "ignore", env: baseEnvironment() });
|
|
1173
|
+
let settled = false;
|
|
1174
|
+
let timer;
|
|
1175
|
+
const finish = (code) => {
|
|
1176
|
+
if (settled)
|
|
1177
|
+
return;
|
|
1178
|
+
settled = true;
|
|
1179
|
+
clearTimeout(timer);
|
|
1180
|
+
resolve3(code);
|
|
1181
|
+
};
|
|
1182
|
+
timer = setTimeout(() => {
|
|
1183
|
+
void stopProcess(probe).catch(() => {
|
|
1184
|
+
}).finally(() => finish(2));
|
|
1185
|
+
probe.kill("SIGKILL");
|
|
1186
|
+
finish(2);
|
|
1187
|
+
}, Math.max(1, timeout));
|
|
1188
|
+
probe.once("error", () => finish(2));
|
|
1189
|
+
probe.once("close", (code) => finish(code ?? 2));
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
async function stopWindowsJob(child, name, closeObserved) {
|
|
1193
|
+
const deadline = performance.now() + 15e3;
|
|
1194
|
+
let timer;
|
|
1195
|
+
try {
|
|
1196
|
+
await Promise.race([
|
|
1197
|
+
stopProcess(child, { closeObserved }),
|
|
1198
|
+
new Promise((_, reject) => {
|
|
1199
|
+
timer = setTimeout(() => {
|
|
1200
|
+
child.kill("SIGKILL");
|
|
1201
|
+
reject(new ACPFailure("cleanup"));
|
|
1202
|
+
}, Math.max(1, deadline - performance.now()));
|
|
1203
|
+
})
|
|
1204
|
+
]);
|
|
1205
|
+
while (performance.now() < deadline) {
|
|
1206
|
+
const state = await jobProbe(name, deadline - performance.now());
|
|
1207
|
+
if (state === 0)
|
|
1208
|
+
return;
|
|
1209
|
+
if (state !== 1)
|
|
1210
|
+
throw new ACPFailure("cleanup");
|
|
1211
|
+
await new Promise((resolve3) => setTimeout(resolve3, 20));
|
|
1212
|
+
}
|
|
1213
|
+
throw new ACPFailure("cleanup");
|
|
1214
|
+
} finally {
|
|
1215
|
+
clearTimeout(timer);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
async function stopProcess(child, options = {}) {
|
|
1219
|
+
if (process2.platform === "win32" && options.windowsJob)
|
|
1220
|
+
return stopWindowsJob(child, options.windowsJob, options.closeObserved);
|
|
1221
|
+
const exited = child.exitCode !== null || child.signalCode !== null;
|
|
1222
|
+
if (!child.pid || exited && !options.ownedTree)
|
|
1223
|
+
return;
|
|
1224
|
+
if (process2.platform === "win32" && exited && options.ownedTree)
|
|
1225
|
+
throw new ACPFailure("cleanup");
|
|
1226
|
+
const closed = options.closeObserved ? Promise.resolve() : new Promise((resolve3) => child.once("close", () => resolve3()));
|
|
1227
|
+
if (process2.platform === "win32") {
|
|
1228
|
+
await new Promise((resolve3, reject) => {
|
|
1229
|
+
const killer = spawn(
|
|
1230
|
+
"taskkill",
|
|
1231
|
+
["/PID", String(child.pid), "/T", "/F"],
|
|
1232
|
+
{ windowsHide: true, stdio: "ignore" }
|
|
1233
|
+
);
|
|
1234
|
+
killer.once("error", reject);
|
|
1235
|
+
killer.once("exit", (code) => code === 0 || !options.ownedTree && child.exitCode !== null ? resolve3() : reject(new ACPFailure("cleanup")));
|
|
1236
|
+
});
|
|
1237
|
+
} else {
|
|
1238
|
+
try {
|
|
1239
|
+
process2.kill(-child.pid, "SIGKILL");
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
if (error.code !== "ESRCH")
|
|
1242
|
+
throw new ACPFailure("cleanup");
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
await closed;
|
|
1246
|
+
}
|
|
1247
|
+
function baseEnvironment() {
|
|
1248
|
+
const output = {};
|
|
1249
|
+
for (const [key, value] of Object.entries(process2.env)) {
|
|
1250
|
+
if (value !== void 0 && /^(?:path|pathext|systemroot|windir|comspec|temp|tmp|lang|lc_all|ssl_cert_file|node_extra_ca_certs)$/i.test(
|
|
1251
|
+
key
|
|
1252
|
+
)) {
|
|
1253
|
+
output[key] = value;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return output;
|
|
1257
|
+
}
|
|
1258
|
+
function engineEnvironment() {
|
|
1259
|
+
const output = baseEnvironment();
|
|
1260
|
+
for (const [key, value] of Object.entries(process2.env)) {
|
|
1261
|
+
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(
|
|
1262
|
+
key
|
|
1263
|
+
)) {
|
|
1264
|
+
output[key] = value;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return output;
|
|
1268
|
+
}
|
|
1269
|
+
async function execute(command, args, cwd, env, timeout = 12e4) {
|
|
1270
|
+
return await new Promise((resolve3, reject) => {
|
|
1271
|
+
const child = spawn(command, args, {
|
|
1272
|
+
cwd,
|
|
1273
|
+
env: {
|
|
1274
|
+
...command === "podman" || command === "docker" ? engineEnvironment() : baseEnvironment(),
|
|
1275
|
+
...env
|
|
1276
|
+
},
|
|
1277
|
+
shell: false,
|
|
1278
|
+
detached: process2.platform !== "win32",
|
|
1279
|
+
windowsHide: true,
|
|
1280
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1281
|
+
});
|
|
1282
|
+
let output = "";
|
|
1283
|
+
let failure;
|
|
1284
|
+
const stop = (error) => {
|
|
1285
|
+
failure ??= error;
|
|
1286
|
+
void stopProcess(child).catch((error2) => {
|
|
1287
|
+
failure = error2;
|
|
1288
|
+
});
|
|
1289
|
+
};
|
|
1290
|
+
const timer = setTimeout(() => stop(new ACPFailure("timeout")), timeout);
|
|
1291
|
+
child.stdout.on("data", (chunk) => {
|
|
1292
|
+
output += chunk.toString("utf8");
|
|
1293
|
+
if (Buffer.byteLength(output) > 8 * 1024 * 1024)
|
|
1294
|
+
stop(new ACPFailure("output"));
|
|
1295
|
+
});
|
|
1296
|
+
child.stderr.resume();
|
|
1297
|
+
child.once("error", (error) => {
|
|
1298
|
+
failure = new ACPFailure(
|
|
1299
|
+
command === "git" && error.code === "ENOENT" ? "gitMissing" : "runtime"
|
|
1300
|
+
);
|
|
1301
|
+
});
|
|
1302
|
+
child.once("close", (code) => {
|
|
1303
|
+
clearTimeout(timer);
|
|
1304
|
+
if (failure)
|
|
1305
|
+
reject(failure);
|
|
1306
|
+
else if (code !== 0)
|
|
1307
|
+
reject(new ACPFailure("failed", { exitCode: code }));
|
|
1308
|
+
else resolve3(output.trim());
|
|
1309
|
+
});
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/acp/ai-acp-client.ts
|
|
1314
|
+
async function connectACP(transport, options = {}, inputLimits) {
|
|
1315
|
+
const limits = ACPConfigSchema.parse(inputLimits ?? {});
|
|
1316
|
+
const handlers = options.handlers ?? {};
|
|
1317
|
+
const capabilities = options.capabilities ?? {};
|
|
1318
|
+
const required2 = [];
|
|
1319
|
+
if (capabilities.fs?.readTextFile)
|
|
1320
|
+
required2.push(methods.client.fs.readTextFile);
|
|
1321
|
+
if (capabilities.fs?.writeTextFile)
|
|
1322
|
+
required2.push(methods.client.fs.writeTextFile);
|
|
1323
|
+
if (capabilities.terminal)
|
|
1324
|
+
required2.push(...Object.values(methods.client.terminal));
|
|
1325
|
+
if (capabilities.elicitation)
|
|
1326
|
+
required2.push(methods.client.elicitation.create);
|
|
1327
|
+
if (required2.some((name) => !Object.hasOwn(handlers, name))) {
|
|
1328
|
+
throw new ACPFailure("invalid");
|
|
1329
|
+
}
|
|
1330
|
+
if (options.signal?.aborted)
|
|
1331
|
+
throw new ACPFailure("cancelled");
|
|
1332
|
+
let stop = async () => {
|
|
1333
|
+
};
|
|
1334
|
+
let stream;
|
|
1335
|
+
if (transport.type === "stdio") {
|
|
1336
|
+
const job = process2.platform === "win32" ? windowsACPCommand(transport.command, transport.args ?? []) : void 0;
|
|
1337
|
+
const child = spawn(job ? "powershell.exe" : transport.command, job?.args ?? transport.args ?? [], {
|
|
1338
|
+
cwd: transport.cwd,
|
|
1339
|
+
env: { ...baseEnvironment(), ...transport.env, ...job?.env },
|
|
1340
|
+
detached: process2.platform !== "win32",
|
|
1341
|
+
shell: false,
|
|
1342
|
+
windowsHide: true,
|
|
1343
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1344
|
+
});
|
|
1345
|
+
let childClosed = false;
|
|
1346
|
+
child.once("close", () => {
|
|
1347
|
+
childClosed = true;
|
|
1348
|
+
});
|
|
1349
|
+
child.stderr.resume();
|
|
1350
|
+
await new Promise((resolve3, reject) => {
|
|
1351
|
+
child.once("spawn", resolve3);
|
|
1352
|
+
child.once("error", () => reject(new ACPFailure("runtime")));
|
|
1353
|
+
});
|
|
1354
|
+
stop = () => stopProcess(child, { ownedTree: true, closeObserved: childClosed, windowsJob: job?.name });
|
|
1355
|
+
stream = ndJsonStream(
|
|
1356
|
+
Writable.toWeb(child.stdin),
|
|
1357
|
+
Readable.toWeb(child.stdout),
|
|
1358
|
+
{ maxMessageBytes: limits.maxContentBytes }
|
|
1359
|
+
);
|
|
1360
|
+
} else if (transport.type === "http") {
|
|
1361
|
+
stream = createHttpStream(transport.url, {
|
|
1362
|
+
headers: transport.headers,
|
|
1363
|
+
maxMessageBytes: limits.maxContentBytes
|
|
1364
|
+
});
|
|
1365
|
+
} else {
|
|
1366
|
+
class BoundedWebSocket extends WebSocket {
|
|
1367
|
+
constructor(url, protocols, options2) {
|
|
1368
|
+
super(url, protocols, {
|
|
1369
|
+
...options2,
|
|
1370
|
+
maxPayload: limits.maxContentBytes
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
stream = createWebSocketStream(transport.url, {
|
|
1375
|
+
headers: transport.headers,
|
|
1376
|
+
WebSocket: BoundedWebSocket
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
const requests = /* @__PURE__ */ new Set();
|
|
1380
|
+
const originalWriter = stream.writable.getWriter();
|
|
1381
|
+
const guardedStream = {
|
|
1382
|
+
writable: new WritableStream({
|
|
1383
|
+
async write(message) {
|
|
1384
|
+
if ("method" in message && "id" in message && message.id !== void 0 && message.id !== null) {
|
|
1385
|
+
requests.add(message.id);
|
|
1386
|
+
}
|
|
1387
|
+
await originalWriter.write(message);
|
|
1388
|
+
},
|
|
1389
|
+
close: () => originalWriter.close(),
|
|
1390
|
+
abort: (reason) => originalWriter.abort(reason)
|
|
1391
|
+
}),
|
|
1392
|
+
readable: stream.readable.pipeThrough(
|
|
1393
|
+
new TransformStream({
|
|
1394
|
+
transform(message, controller) {
|
|
1395
|
+
if (!("method" in message) && "id" in message && message.id !== void 0 && message.id !== null) {
|
|
1396
|
+
requests.delete(message.id);
|
|
1397
|
+
}
|
|
1398
|
+
controller.enqueue(message);
|
|
1399
|
+
}
|
|
1400
|
+
})
|
|
1401
|
+
)
|
|
1402
|
+
};
|
|
1403
|
+
let sessionId;
|
|
1404
|
+
let sessionOpening = false;
|
|
1405
|
+
let closed = false;
|
|
1406
|
+
let failure;
|
|
1407
|
+
let closePromise;
|
|
1408
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1409
|
+
const app = client({ name: "hai-ai" });
|
|
1410
|
+
app.onNotification(methods.client.session.update, ({ params }) => {
|
|
1411
|
+
if (sessionId && params.sessionId !== sessionId)
|
|
1412
|
+
return;
|
|
1413
|
+
try {
|
|
1414
|
+
options.onEvent?.(params);
|
|
1415
|
+
for (const listener of listeners) listener(params);
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
failure = error;
|
|
1418
|
+
void close().catch(() => {
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
function owned(id) {
|
|
1423
|
+
if (!sessionId || id !== sessionId)
|
|
1424
|
+
throw new ACPFailure("missing");
|
|
1425
|
+
}
|
|
1426
|
+
app.onRequest(methods.client.session.requestPermission, async (context) => {
|
|
1427
|
+
owned(context.params.sessionId);
|
|
1428
|
+
const handler = handlers["session/request_permission"];
|
|
1429
|
+
if (!handler)
|
|
1430
|
+
return { outcome: { outcome: "cancelled" } };
|
|
1431
|
+
return handler(context);
|
|
1432
|
+
});
|
|
1433
|
+
app.onRequest(methods.client.fs.readTextFile, async (context) => {
|
|
1434
|
+
owned(context.params.sessionId);
|
|
1435
|
+
const handler = handlers["fs/read_text_file"];
|
|
1436
|
+
if (!handler)
|
|
1437
|
+
throw new ACPFailure("unsupported");
|
|
1438
|
+
return handler(context);
|
|
1439
|
+
});
|
|
1440
|
+
app.onRequest(methods.client.fs.writeTextFile, async (context) => {
|
|
1441
|
+
owned(context.params.sessionId);
|
|
1442
|
+
const handler = handlers["fs/write_text_file"];
|
|
1443
|
+
if (!handler)
|
|
1444
|
+
throw new ACPFailure("unsupported");
|
|
1445
|
+
return handler(context);
|
|
1446
|
+
});
|
|
1447
|
+
app.onRequest(methods.client.terminal.create, async (context) => {
|
|
1448
|
+
owned(context.params.sessionId);
|
|
1449
|
+
const handler = handlers["terminal/create"];
|
|
1450
|
+
if (!handler)
|
|
1451
|
+
throw new ACPFailure("unsupported");
|
|
1452
|
+
return handler(context);
|
|
1453
|
+
});
|
|
1454
|
+
app.onRequest(methods.client.terminal.output, async (context) => {
|
|
1455
|
+
owned(context.params.sessionId);
|
|
1456
|
+
const handler = handlers["terminal/output"];
|
|
1457
|
+
if (!handler)
|
|
1458
|
+
throw new ACPFailure("unsupported");
|
|
1459
|
+
return handler(context);
|
|
1460
|
+
});
|
|
1461
|
+
app.onRequest(methods.client.terminal.release, async (context) => {
|
|
1462
|
+
owned(context.params.sessionId);
|
|
1463
|
+
const handler = handlers["terminal/release"];
|
|
1464
|
+
if (!handler)
|
|
1465
|
+
throw new ACPFailure("unsupported");
|
|
1466
|
+
return handler(context);
|
|
1467
|
+
});
|
|
1468
|
+
app.onRequest(methods.client.terminal.waitForExit, async (context) => {
|
|
1469
|
+
owned(context.params.sessionId);
|
|
1470
|
+
const handler = handlers["terminal/wait_for_exit"];
|
|
1471
|
+
if (!handler)
|
|
1472
|
+
throw new ACPFailure("unsupported");
|
|
1473
|
+
return handler(context);
|
|
1474
|
+
});
|
|
1475
|
+
app.onRequest(methods.client.terminal.kill, async (context) => {
|
|
1476
|
+
owned(context.params.sessionId);
|
|
1477
|
+
const handler = handlers["terminal/kill"];
|
|
1478
|
+
if (!handler)
|
|
1479
|
+
throw new ACPFailure("unsupported");
|
|
1480
|
+
return handler(context);
|
|
1481
|
+
});
|
|
1482
|
+
app.onRequest(methods.client.elicitation.create, async (context) => {
|
|
1483
|
+
if ("sessionId" in context.params && typeof context.params.sessionId === "string") {
|
|
1484
|
+
owned(context.params.sessionId);
|
|
1485
|
+
} else if (!("requestId" in context.params) || typeof context.params.requestId !== "string" && typeof context.params.requestId !== "number" || !requests.has(context.params.requestId)) {
|
|
1486
|
+
throw new ACPFailure("missing");
|
|
1487
|
+
}
|
|
1488
|
+
const handler = handlers["elicitation/create"];
|
|
1489
|
+
if (!handler)
|
|
1490
|
+
throw new ACPFailure("unsupported");
|
|
1491
|
+
return handler(context);
|
|
1492
|
+
});
|
|
1493
|
+
const connection = app.connect(guardedStream);
|
|
1494
|
+
function close() {
|
|
1495
|
+
closed = true;
|
|
1496
|
+
if (!closePromise) {
|
|
1497
|
+
const task = (async () => {
|
|
1498
|
+
try {
|
|
1499
|
+
await stop();
|
|
1500
|
+
} finally {
|
|
1501
|
+
connection.close();
|
|
1502
|
+
}
|
|
1503
|
+
})();
|
|
1504
|
+
closePromise = task;
|
|
1505
|
+
void task.catch(() => {
|
|
1506
|
+
if (closePromise === task)
|
|
1507
|
+
closePromise = void 0;
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
return closePromise;
|
|
1511
|
+
}
|
|
1512
|
+
async function request(operation, timeout = limits.requestTimeout, signal, prompt = false, isCancelledResponse) {
|
|
1513
|
+
if (closed)
|
|
1514
|
+
throw new ACPFailure("closed");
|
|
1515
|
+
if (signal?.aborted)
|
|
1516
|
+
throw new ACPFailure("cancelled");
|
|
1517
|
+
let timer;
|
|
1518
|
+
let abort = () => {
|
|
1519
|
+
};
|
|
1520
|
+
let reason;
|
|
1521
|
+
const deadline = new Promise((_, reject) => {
|
|
1522
|
+
const fail = (kind) => {
|
|
1523
|
+
if (reason)
|
|
1524
|
+
return;
|
|
1525
|
+
reason = new ACPFailure(kind);
|
|
1526
|
+
if (prompt && sessionId) {
|
|
1527
|
+
void connection.agent.notify(methods.agent.session.cancel, { sessionId }).catch(() => {
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
clearTimeout(timer);
|
|
1531
|
+
timer = setTimeout(
|
|
1532
|
+
() => {
|
|
1533
|
+
void close().then(() => reject(reason), reject);
|
|
1534
|
+
},
|
|
1535
|
+
prompt ? limits.cancelTimeout : 0
|
|
1536
|
+
);
|
|
1537
|
+
};
|
|
1538
|
+
timer = setTimeout(fail, timeout, "timeout");
|
|
1539
|
+
abort = () => fail("cancelled");
|
|
1540
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1541
|
+
});
|
|
1542
|
+
try {
|
|
1543
|
+
const output = await Promise.race([operation(), deadline]);
|
|
1544
|
+
if (failure)
|
|
1545
|
+
throw failure;
|
|
1546
|
+
if (reason) {
|
|
1547
|
+
if (!(reason.kind === "cancelled" && prompt && isCancelledResponse?.(output)))
|
|
1548
|
+
await close();
|
|
1549
|
+
throw reason;
|
|
1550
|
+
}
|
|
1551
|
+
return output;
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
if (failure)
|
|
1554
|
+
throw failure;
|
|
1555
|
+
if (reason)
|
|
1556
|
+
throw reason;
|
|
1557
|
+
throw error;
|
|
1558
|
+
} finally {
|
|
1559
|
+
clearTimeout(timer);
|
|
1560
|
+
signal?.removeEventListener("abort", abort);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
try {
|
|
1564
|
+
let checkMcp2 = function(servers) {
|
|
1565
|
+
for (const server of servers) {
|
|
1566
|
+
if ("type" in server && server.type === "http" && !agentCapabilities.mcpCapabilities?.http) {
|
|
1567
|
+
throw new ACPFailure("unsupported");
|
|
1568
|
+
}
|
|
1569
|
+
if ("type" in server && server.type === "sse" && !agentCapabilities.mcpCapabilities?.sse) {
|
|
1570
|
+
throw new ACPFailure("unsupported");
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
var checkMcp = checkMcp2;
|
|
1575
|
+
const initialized = await request(
|
|
1576
|
+
() => connection.agent.request(methods.agent.initialize, {
|
|
1577
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1578
|
+
clientCapabilities: capabilities
|
|
1579
|
+
}),
|
|
1580
|
+
limits.requestTimeout,
|
|
1581
|
+
options.signal
|
|
1582
|
+
);
|
|
1583
|
+
if (initialized.protocolVersion !== PROTOCOL_VERSION)
|
|
1584
|
+
throw new ACPFailure("unsupported");
|
|
1585
|
+
const agentCapabilities = initialized.agentCapabilities ?? {};
|
|
1586
|
+
let activePrompt;
|
|
1587
|
+
async function prompt(content, signal) {
|
|
1588
|
+
if (activePrompt)
|
|
1589
|
+
throw new ACPFailure("busy");
|
|
1590
|
+
if (!sessionId)
|
|
1591
|
+
throw new ACPFailure("missing");
|
|
1592
|
+
if (Buffer.byteLength(JSON.stringify(content)) > limits.maxContentBytes)
|
|
1593
|
+
throw new ACPFailure("output");
|
|
1594
|
+
for (const item of content) {
|
|
1595
|
+
if (item.type === "image" && !agentCapabilities.promptCapabilities?.image || item.type === "audio" && !agentCapabilities.promptCapabilities?.audio || item.type === "resource" && !agentCapabilities.promptCapabilities?.embeddedContext) {
|
|
1596
|
+
throw new ACPFailure("unsupported");
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
const events = [];
|
|
1600
|
+
let bytes = 0;
|
|
1601
|
+
let text = "";
|
|
1602
|
+
let messageId;
|
|
1603
|
+
const listener = (event) => {
|
|
1604
|
+
bytes += Buffer.byteLength(JSON.stringify(event));
|
|
1605
|
+
if (events.length >= limits.maxPendingEvents || bytes > limits.maxContentBytes) {
|
|
1606
|
+
throw new ACPFailure("output");
|
|
1607
|
+
}
|
|
1608
|
+
events.push(event);
|
|
1609
|
+
if (event.update.sessionUpdate === "agent_message_chunk" && event.update.content.type === "text") {
|
|
1610
|
+
if (event.update.messageId && event.update.messageId !== messageId)
|
|
1611
|
+
text = "";
|
|
1612
|
+
messageId = event.update.messageId;
|
|
1613
|
+
text += event.update.content.text;
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
listeners.add(listener);
|
|
1617
|
+
activePrompt = new AbortController();
|
|
1618
|
+
try {
|
|
1619
|
+
const response = await request(
|
|
1620
|
+
() => connection.agent.request(methods.agent.session.prompt, {
|
|
1621
|
+
sessionId,
|
|
1622
|
+
prompt: content
|
|
1623
|
+
}),
|
|
1624
|
+
limits.promptTimeout,
|
|
1625
|
+
signal ? AbortSignal.any([activePrompt.signal, signal]) : activePrompt.signal,
|
|
1626
|
+
true,
|
|
1627
|
+
(output) => output.stopReason === "cancelled"
|
|
1628
|
+
);
|
|
1629
|
+
if (response.stopReason === "cancelled")
|
|
1630
|
+
throw new ACPFailure("cancelled");
|
|
1631
|
+
return { ...response, text, events };
|
|
1632
|
+
} finally {
|
|
1633
|
+
listeners.delete(listener);
|
|
1634
|
+
activePrompt = void 0;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return {
|
|
1638
|
+
initialized,
|
|
1639
|
+
/**
|
|
1640
|
+
* 关闭传输并终止拥有的进程树,可重复调用。
|
|
1641
|
+
* @returns 清理完成的 Promise;清理失败抛错。
|
|
1642
|
+
* @example
|
|
1643
|
+
* ```ts
|
|
1644
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
1645
|
+
* await connection.close()
|
|
1646
|
+
* ```
|
|
1647
|
+
*/
|
|
1648
|
+
close,
|
|
1649
|
+
/**
|
|
1650
|
+
* 执行单轮原生请求并收集有限事件。
|
|
1651
|
+
* @param content - 原生内容块。
|
|
1652
|
+
* @param signal - 本轮取消信号。
|
|
1653
|
+
* @returns 最终文本、停止原因及事件;业务边界负责转换异常。
|
|
1654
|
+
* @example
|
|
1655
|
+
* ```ts
|
|
1656
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
1657
|
+
* const response = await connection.prompt([{ type: 'text', text: '检查工程' }])
|
|
1658
|
+
* ```
|
|
1659
|
+
*/
|
|
1660
|
+
prompt,
|
|
1661
|
+
/**
|
|
1662
|
+
* 订阅会话事件;返回取消订阅函数。
|
|
1663
|
+
* @param listener - 消费原生事件的回调;抛错会关闭连接。
|
|
1664
|
+
* @returns 取消订阅函数。
|
|
1665
|
+
* @example
|
|
1666
|
+
* ```ts
|
|
1667
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1668
|
+
* const unsubscribe = connection.subscribe(event => consume(event)); unsubscribe()
|
|
1669
|
+
* ```
|
|
1670
|
+
*/
|
|
1671
|
+
subscribe(listener) {
|
|
1672
|
+
listeners.add(listener);
|
|
1673
|
+
return () => listeners.delete(listener);
|
|
1674
|
+
},
|
|
1675
|
+
/**
|
|
1676
|
+
* 发送原生取消通知并触发正在执行轮次的取消期限。
|
|
1677
|
+
* @returns 通知发送后的 Promise;本轮终止由 prompt 的取消期限保证。
|
|
1678
|
+
* @example
|
|
1679
|
+
* ```ts
|
|
1680
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1681
|
+
* await connection.cancel()
|
|
1682
|
+
* ```
|
|
1683
|
+
*/
|
|
1684
|
+
cancel: async () => {
|
|
1685
|
+
activePrompt?.abort();
|
|
1686
|
+
if (sessionId && !closed) {
|
|
1687
|
+
await connection.agent.notify(methods.agent.session.cancel, {
|
|
1688
|
+
sessionId
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
},
|
|
1692
|
+
/**
|
|
1693
|
+
* 选择握手 authMethods 中的认证方法,凭据由原生进程或远程服务提供。
|
|
1694
|
+
* @param methodId - 对端握手声明的认证方法。
|
|
1695
|
+
* @returns 原生认证响应。
|
|
1696
|
+
* @example
|
|
1697
|
+
* ```ts
|
|
1698
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1699
|
+
* await connection.authenticate('api-key')
|
|
1700
|
+
* ```
|
|
1701
|
+
*/
|
|
1702
|
+
authenticate: (methodId) => request(
|
|
1703
|
+
() => connection.agent.request(methods.agent.authenticate, { methodId })
|
|
1704
|
+
),
|
|
1705
|
+
/**
|
|
1706
|
+
* 创建此连接唯一的原生会话;MCP 类型必须由智能体声明支持。
|
|
1707
|
+
* @param cwd - 对端可见的绝对工作目录。
|
|
1708
|
+
* @param mcpServers - 原生 MCP 定义,默认空列表。
|
|
1709
|
+
* @returns 包含原生 sessionId 的新建响应。
|
|
1710
|
+
* @example
|
|
1711
|
+
* ```ts
|
|
1712
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1713
|
+
* await connection.createSession('/srv/project', [])
|
|
1714
|
+
* ```
|
|
1715
|
+
*/
|
|
1716
|
+
async createSession(cwd, mcpServers = []) {
|
|
1717
|
+
if (sessionId || sessionOpening)
|
|
1718
|
+
throw new ACPFailure("busy");
|
|
1719
|
+
sessionOpening = true;
|
|
1720
|
+
try {
|
|
1721
|
+
checkMcp2(mcpServers);
|
|
1722
|
+
const response = await request(
|
|
1723
|
+
() => connection.agent.request(methods.agent.session.new, {
|
|
1724
|
+
cwd,
|
|
1725
|
+
mcpServers
|
|
1726
|
+
})
|
|
1727
|
+
);
|
|
1728
|
+
sessionId = response.sessionId;
|
|
1729
|
+
return response;
|
|
1730
|
+
} finally {
|
|
1731
|
+
sessionOpening = false;
|
|
1732
|
+
}
|
|
1733
|
+
},
|
|
1734
|
+
/**
|
|
1735
|
+
* 优先调用原生 resume,否则使用已声明的 load;均不支持则失败。
|
|
1736
|
+
* @param id - 原生会话 ID。
|
|
1737
|
+
* @param cwd - 原会话绝对工作目录。
|
|
1738
|
+
* @param mcpServers - 当前原生 MCP 定义。
|
|
1739
|
+
* @returns 恢复或加载响应;能力缺失时抛错。
|
|
1740
|
+
* @example
|
|
1741
|
+
* ```ts
|
|
1742
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1743
|
+
* await connection.resumeSession('native-id', '/srv/project')
|
|
1744
|
+
* ```
|
|
1745
|
+
*/
|
|
1746
|
+
async resumeSession(id, cwd, mcpServers = []) {
|
|
1747
|
+
if (sessionId || sessionOpening)
|
|
1748
|
+
throw new ACPFailure("busy");
|
|
1749
|
+
checkMcp2(mcpServers);
|
|
1750
|
+
sessionId = id;
|
|
1751
|
+
if (agentCapabilities.sessionCapabilities?.resume) {
|
|
1752
|
+
return request(
|
|
1753
|
+
() => connection.agent.request(methods.agent.session.resume, {
|
|
1754
|
+
sessionId: id,
|
|
1755
|
+
cwd,
|
|
1756
|
+
mcpServers
|
|
1757
|
+
})
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
if (agentCapabilities.loadSession) {
|
|
1761
|
+
return request(
|
|
1762
|
+
() => connection.agent.request(methods.agent.session.load, {
|
|
1763
|
+
sessionId: id,
|
|
1764
|
+
cwd,
|
|
1765
|
+
mcpServers
|
|
1766
|
+
})
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
throw new ACPFailure("unsupported");
|
|
1770
|
+
},
|
|
1771
|
+
/**
|
|
1772
|
+
* 按智能体原生游标列出会话;未声明 list 能力则失败。
|
|
1773
|
+
* @param cursor - 可选分页游标。
|
|
1774
|
+
* @returns 原生分页会话列表。
|
|
1775
|
+
* @example
|
|
1776
|
+
* ```ts
|
|
1777
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1778
|
+
* const page = await connection.listSessions()
|
|
1779
|
+
* ```
|
|
1780
|
+
*/
|
|
1781
|
+
async listSessions(cursor) {
|
|
1782
|
+
if (!agentCapabilities.sessionCapabilities?.list)
|
|
1783
|
+
throw new ACPFailure("unsupported");
|
|
1784
|
+
return request(
|
|
1785
|
+
() => connection.agent.request(methods.agent.session.list, { cursor })
|
|
1786
|
+
);
|
|
1787
|
+
},
|
|
1788
|
+
/**
|
|
1789
|
+
* 要求远端删除指定原生会话;不代替框架工作区清理。
|
|
1790
|
+
* @param id - 原生会话 ID。
|
|
1791
|
+
* @returns 原生删除响应。
|
|
1792
|
+
* @example
|
|
1793
|
+
* ```ts
|
|
1794
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1795
|
+
* await connection.deleteSession('native-id')
|
|
1796
|
+
* ```
|
|
1797
|
+
*/
|
|
1798
|
+
async deleteSession(id) {
|
|
1799
|
+
if (!agentCapabilities.sessionCapabilities?.delete)
|
|
1800
|
+
throw new ACPFailure("unsupported");
|
|
1801
|
+
return request(
|
|
1802
|
+
() => connection.agent.request(methods.agent.session.delete, {
|
|
1803
|
+
sessionId: id
|
|
1804
|
+
})
|
|
1805
|
+
);
|
|
1806
|
+
},
|
|
1807
|
+
/**
|
|
1808
|
+
* 若远端声明 close 能力则关闭原生会话;进程停止由 close 负责。
|
|
1809
|
+
* @returns 原生关闭完成的 Promise;未声明能力时无需请求。
|
|
1810
|
+
* @example
|
|
1811
|
+
* ```ts
|
|
1812
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1813
|
+
* await connection.closeSession()
|
|
1814
|
+
* ```
|
|
1815
|
+
*/
|
|
1816
|
+
async closeSession() {
|
|
1817
|
+
if (!closed && sessionId && agentCapabilities.sessionCapabilities?.close) {
|
|
1818
|
+
await request(
|
|
1819
|
+
() => connection.agent.request(methods.agent.session.close, {
|
|
1820
|
+
sessionId
|
|
1821
|
+
})
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
},
|
|
1825
|
+
/**
|
|
1826
|
+
* 给当前原生会话设置智能体公布的模式。
|
|
1827
|
+
* @param modeId - 原生模式 ID。
|
|
1828
|
+
* @returns 原生模式响应。
|
|
1829
|
+
* @example
|
|
1830
|
+
* ```ts
|
|
1831
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1832
|
+
* await connection.setMode('code')
|
|
1833
|
+
* ```
|
|
1834
|
+
*/
|
|
1835
|
+
setMode: (modeId) => request(
|
|
1836
|
+
() => connection.agent.request(methods.agent.session.setMode, {
|
|
1837
|
+
sessionId,
|
|
1838
|
+
modeId
|
|
1839
|
+
})
|
|
1840
|
+
),
|
|
1841
|
+
/**
|
|
1842
|
+
* 给当前原生会话设置智能体公布的配置选项。
|
|
1843
|
+
* @param configId - 原生配置选项 ID。
|
|
1844
|
+
* @param value - 智能体允许的选项值。
|
|
1845
|
+
* @returns 原生配置响应。
|
|
1846
|
+
* @example
|
|
1847
|
+
* ```ts
|
|
1848
|
+
* // 模块内部,connection 为 connectACP 返回值,异常由业务边界转换。
|
|
1849
|
+
* await connection.setConfigOption('model', 'coding')
|
|
1850
|
+
* ```
|
|
1851
|
+
*/
|
|
1852
|
+
setConfigOption: (configId, value) => request(
|
|
1853
|
+
() => connection.agent.request(methods.agent.session.setConfigOption, {
|
|
1854
|
+
sessionId,
|
|
1855
|
+
configId,
|
|
1856
|
+
value
|
|
1857
|
+
})
|
|
1858
|
+
)
|
|
1859
|
+
};
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
await close();
|
|
1862
|
+
throw error;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// src/acp/ai-acp-git.ts
|
|
1867
|
+
function createGitOperations(workspace, runtime) {
|
|
1868
|
+
const git = (cwd, args, env) => runtime.run("git", ["-C", cwd, ...args], void 0, {
|
|
1869
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1870
|
+
...env
|
|
1871
|
+
});
|
|
1872
|
+
const source = (id) => runtime.path(`main/${id}`);
|
|
1873
|
+
async function ready() {
|
|
1874
|
+
if (!workspace.repositories.length)
|
|
1875
|
+
return;
|
|
1876
|
+
try {
|
|
1877
|
+
await runtime.run("git", ["--version"]);
|
|
1878
|
+
} catch {
|
|
1879
|
+
throw new ACPFailure("gitMissing");
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
async function clean(path) {
|
|
1883
|
+
if (await git(path, ["status", "--porcelain"]))
|
|
1884
|
+
throw new ACPFailure("conflict");
|
|
1885
|
+
}
|
|
1886
|
+
async function supportedContent(path) {
|
|
1887
|
+
const index = await git(path, ["ls-files", "--stage", "-z"]);
|
|
1888
|
+
if (index.split("\0").some((entry) => entry.startsWith("160000 ")))
|
|
1889
|
+
throw new ACPFailure("unsupported");
|
|
1890
|
+
const attributes = await git(path, ["ls-files", "-z", "--", ".gitattributes", ":(glob)**/.gitattributes"]);
|
|
1891
|
+
for (const file of attributes.split("\0").filter(Boolean)) {
|
|
1892
|
+
const content = await git(path, ["show", `HEAD:${file}`]);
|
|
1893
|
+
if (content.split("\n").some((line) => !line.trimStart().startsWith("#") && /\bfilter=lfs\b/.test(line)))
|
|
1894
|
+
throw new ACPFailure("unsupported");
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
async function checked(repo, path = source(repo.id), branch = repo.mainBranch) {
|
|
1898
|
+
if (await git(path, ["branch", "--show-current"]) !== branch)
|
|
1899
|
+
throw new ACPFailure("conflict");
|
|
1900
|
+
if (await git(path, ["remote", "get-url", repo.remote]) !== repo.url)
|
|
1901
|
+
throw new ACPFailure("conflict");
|
|
1902
|
+
await runtime.file("exists", path);
|
|
1903
|
+
}
|
|
1904
|
+
async function reports(operation) {
|
|
1905
|
+
await ready();
|
|
1906
|
+
const output = [];
|
|
1907
|
+
for (const repo of workspace.repositories) {
|
|
1908
|
+
const report = {
|
|
1909
|
+
repositoryId: repo.id,
|
|
1910
|
+
success: false,
|
|
1911
|
+
phase: "check"
|
|
1912
|
+
};
|
|
1913
|
+
try {
|
|
1914
|
+
await operation(repo, report);
|
|
1915
|
+
report.success = true;
|
|
1916
|
+
report.phase = "done";
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
report.error = error instanceof ACPFailure ? error.kind : "failed";
|
|
1919
|
+
}
|
|
1920
|
+
output.push(report);
|
|
1921
|
+
}
|
|
1922
|
+
if (output.some((item) => !item.success))
|
|
1923
|
+
throw new ACPFailure("partial", { reports: output });
|
|
1924
|
+
return output;
|
|
1925
|
+
}
|
|
1926
|
+
return {
|
|
1927
|
+
/**
|
|
1928
|
+
* 准备工程主目录,只克隆缺失仓库,不覆盖已有内容。
|
|
1929
|
+
* @returns main 的绝对路径;失败保留已创建文件。
|
|
1930
|
+
* @example
|
|
1931
|
+
* ```ts
|
|
1932
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
1933
|
+
* const main = await git.prepare()
|
|
1934
|
+
* ```
|
|
1935
|
+
*/
|
|
1936
|
+
async prepare() {
|
|
1937
|
+
await ready();
|
|
1938
|
+
await runtime.file("mkdir", "main/files");
|
|
1939
|
+
for (const repo of workspace.repositories) {
|
|
1940
|
+
await git(runtime.root, [
|
|
1941
|
+
"check-ref-format",
|
|
1942
|
+
"--branch",
|
|
1943
|
+
repo.mainBranch
|
|
1944
|
+
]);
|
|
1945
|
+
if (await runtime.file("exists", `main/${repo.id}`) === "false") {
|
|
1946
|
+
await git(runtime.root, [
|
|
1947
|
+
"clone",
|
|
1948
|
+
"--origin",
|
|
1949
|
+
repo.remote,
|
|
1950
|
+
"--branch",
|
|
1951
|
+
repo.mainBranch,
|
|
1952
|
+
"--",
|
|
1953
|
+
repo.url,
|
|
1954
|
+
source(repo.id)
|
|
1955
|
+
]);
|
|
1956
|
+
}
|
|
1957
|
+
await checked(repo);
|
|
1958
|
+
await supportedContent(source(repo.id));
|
|
1959
|
+
}
|
|
1960
|
+
return runtime.path("main");
|
|
1961
|
+
},
|
|
1962
|
+
/**
|
|
1963
|
+
* 从每个工程本地主分支创建独立工作树并复制普通文件。
|
|
1964
|
+
* @param sessionId - 已校验的框架会话 ID。
|
|
1965
|
+
* @param branch - 所有工程使用的同名新分支。
|
|
1966
|
+
* @returns 独立会话目录;中途失败不回滚已有工作树。
|
|
1967
|
+
* @example
|
|
1968
|
+
* ```ts
|
|
1969
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
1970
|
+
* const directory = await git.createWorktree('task-1', 'acp/task-1')
|
|
1971
|
+
* ```
|
|
1972
|
+
*/
|
|
1973
|
+
async createWorktree(sessionId, branch) {
|
|
1974
|
+
await ready();
|
|
1975
|
+
await runtime.file("mkdir", `sessions/${sessionId}`);
|
|
1976
|
+
for (const repo of workspace.repositories) {
|
|
1977
|
+
await checked(repo);
|
|
1978
|
+
await supportedContent(source(repo.id));
|
|
1979
|
+
await git(source(repo.id), ["check-ref-format", "--branch", branch]);
|
|
1980
|
+
await git(source(repo.id), [
|
|
1981
|
+
"worktree",
|
|
1982
|
+
"add",
|
|
1983
|
+
"-b",
|
|
1984
|
+
branch,
|
|
1985
|
+
runtime.path(`sessions/${sessionId}/${repo.id}`),
|
|
1986
|
+
`refs/heads/${repo.mainBranch}`
|
|
1987
|
+
]);
|
|
1988
|
+
}
|
|
1989
|
+
await runtime.file("copy", "main/files", `sessions/${sessionId}/files`);
|
|
1990
|
+
return runtime.path(`sessions/${sessionId}`);
|
|
1991
|
+
},
|
|
1992
|
+
/**
|
|
1993
|
+
* 更新各工程主分支,只允许快进合并。
|
|
1994
|
+
* @returns 每个工程的更新报告;部分失败抛出携带 reports 的异常。
|
|
1995
|
+
* @example
|
|
1996
|
+
* ```ts
|
|
1997
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
1998
|
+
* const reports = await git.updateMain()
|
|
1999
|
+
* ```
|
|
2000
|
+
*/
|
|
2001
|
+
updateMain: () => reports(async (repo, report) => {
|
|
2002
|
+
const path = source(repo.id);
|
|
2003
|
+
await checked(repo);
|
|
2004
|
+
await clean(path);
|
|
2005
|
+
report.phase = "fetch";
|
|
2006
|
+
await git(path, [
|
|
2007
|
+
"fetch",
|
|
2008
|
+
"--",
|
|
2009
|
+
repo.remote,
|
|
2010
|
+
`refs/heads/${repo.mainBranch}`
|
|
2011
|
+
]);
|
|
2012
|
+
report.phase = "merge";
|
|
2013
|
+
await git(path, ["merge", "--ff-only", "FETCH_HEAD"]);
|
|
2014
|
+
report.commit = await git(path, ["rev-parse", "HEAD"]);
|
|
2015
|
+
}),
|
|
2016
|
+
/**
|
|
2017
|
+
* 逐仓提交会话修改并发布,禁止强推。
|
|
2018
|
+
* @param sessionId - 已登记的独立会话 ID。
|
|
2019
|
+
* @param branch - 会话工作树分支名。
|
|
2020
|
+
* @param request - 经过 schema 校验的模式、提交说明及作者。
|
|
2021
|
+
* @returns 各仓发布报告;部分失败保留已提交和已推送的结果。
|
|
2022
|
+
* @example
|
|
2023
|
+
* ```ts
|
|
2024
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2025
|
+
* const reports = await git.publish('task-1', 'acp/task-1', { mode: 'push-branch', message: '完成任务', author: { name: 'Developer', email: 'dev@example.com' } })
|
|
2026
|
+
* ```
|
|
2027
|
+
*/
|
|
2028
|
+
publish: (sessionId, branch, request) => reports(async (repo, report) => {
|
|
2029
|
+
const session = runtime.path(`sessions/${sessionId}/${repo.id}`);
|
|
2030
|
+
const main = source(repo.id);
|
|
2031
|
+
const author = {
|
|
2032
|
+
GIT_AUTHOR_NAME: request.author.name,
|
|
2033
|
+
GIT_AUTHOR_EMAIL: request.author.email,
|
|
2034
|
+
GIT_COMMITTER_NAME: request.author.name,
|
|
2035
|
+
GIT_COMMITTER_EMAIL: request.author.email
|
|
2036
|
+
};
|
|
2037
|
+
await checked(repo, session, branch);
|
|
2038
|
+
if (request.mode === "merge-main") {
|
|
2039
|
+
await checked(repo);
|
|
2040
|
+
await clean(main);
|
|
2041
|
+
}
|
|
2042
|
+
report.phase = "commit";
|
|
2043
|
+
if (await git(session, ["status", "--porcelain"])) {
|
|
2044
|
+
await git(session, ["add", "--all"]);
|
|
2045
|
+
await git(session, ["commit", "-m", request.message], author);
|
|
2046
|
+
}
|
|
2047
|
+
report.commit = await git(session, ["rev-parse", "HEAD"]);
|
|
2048
|
+
let target = session;
|
|
2049
|
+
let targetBranch = branch;
|
|
2050
|
+
if (request.mode === "merge-main") {
|
|
2051
|
+
report.phase = "fetch";
|
|
2052
|
+
await git(main, [
|
|
2053
|
+
"fetch",
|
|
2054
|
+
"--",
|
|
2055
|
+
repo.remote,
|
|
2056
|
+
`refs/heads/${repo.mainBranch}`
|
|
2057
|
+
]);
|
|
2058
|
+
report.phase = "merge";
|
|
2059
|
+
const remoteHead = await git(main, ["rev-parse", "FETCH_HEAD"]);
|
|
2060
|
+
const localHead = await git(main, ["rev-parse", "HEAD"]);
|
|
2061
|
+
const base = await git(main, ["merge-base", "HEAD", "FETCH_HEAD"]);
|
|
2062
|
+
if (base === localHead)
|
|
2063
|
+
await git(main, ["merge", "--ff-only", "FETCH_HEAD"]);
|
|
2064
|
+
else if (base !== remoteHead)
|
|
2065
|
+
throw new ACPFailure("conflict");
|
|
2066
|
+
await git(main, ["merge", "--no-edit", "--", branch], author);
|
|
2067
|
+
target = main;
|
|
2068
|
+
targetBranch = repo.mainBranch;
|
|
2069
|
+
}
|
|
2070
|
+
report.phase = "push";
|
|
2071
|
+
await git(target, [
|
|
2072
|
+
"push",
|
|
2073
|
+
"--",
|
|
2074
|
+
repo.remote,
|
|
2075
|
+
`HEAD:refs/heads/${targetBranch}`
|
|
2076
|
+
]);
|
|
2077
|
+
report.commit = await git(target, ["rev-parse", "HEAD"]);
|
|
2078
|
+
})
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
var fileProgram = String.raw`
|
|
2082
|
+
const fs = require('node:fs/promises'); const path = require('node:path');
|
|
2083
|
+
(async () => {
|
|
2084
|
+
const [root, op, name, content] = process.argv.slice(1);
|
|
2085
|
+
const base = await fs.realpath(root);
|
|
2086
|
+
const target = path.resolve(base, name || '.');
|
|
2087
|
+
const rel = path.relative(base, target);
|
|
2088
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) throw Error('scope');
|
|
2089
|
+
let cursor = base;
|
|
2090
|
+
for (const part of rel.split(path.sep).filter(Boolean)) {
|
|
2091
|
+
cursor = path.join(cursor, part);
|
|
2092
|
+
try { if ((await fs.lstat(cursor)).isSymbolicLink()) throw Error('link'); }
|
|
2093
|
+
catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
2094
|
+
}
|
|
2095
|
+
if (op === 'mkdir') await fs.mkdir(target, { recursive: true });
|
|
2096
|
+
else if (op === 'exists') process.stdout.write(String(await fs.stat(target).then(() => true, e => { if(e.code === 'ENOENT') return false; throw e; })));
|
|
2097
|
+
else if (op === 'write') { await fs.mkdir(path.dirname(target), { recursive: true }); await fs.writeFile(target, Buffer.from(content, 'base64'), { flag: 'wx', mode: 0o600 }); }
|
|
2098
|
+
else if (op === 'read') process.stdout.write((await fs.readFile(target)).toString('base64'));
|
|
2099
|
+
else if (op === 'copy') {
|
|
2100
|
+
const destination = path.resolve(base, content);
|
|
2101
|
+
const destRel = path.relative(base, destination);
|
|
2102
|
+
if(destRel.startsWith('..') || path.isAbsolute(destRel)) throw Error('scope');
|
|
2103
|
+
let destCursor = base;
|
|
2104
|
+
for (const part of destRel.split(path.sep).filter(Boolean)) {
|
|
2105
|
+
destCursor = path.join(destCursor, part);
|
|
2106
|
+
try { if ((await fs.lstat(destCursor)).isSymbolicLink()) throw Error('link'); }
|
|
2107
|
+
catch(e) { if(e.code !== 'ENOENT') throw e; }
|
|
2108
|
+
}
|
|
2109
|
+
async function copy(src, dst) {
|
|
2110
|
+
try { if ((await fs.lstat(dst)).isSymbolicLink()) throw Error('link'); }
|
|
2111
|
+
catch(e) { if(e.code !== 'ENOENT') throw e; }
|
|
2112
|
+
const stat = await fs.lstat(src); if(stat.isSymbolicLink()) throw Error('link');
|
|
2113
|
+
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)); }
|
|
2114
|
+
else if(stat.isFile()) await fs.copyFile(src,dst,require('node:fs').constants.COPYFILE_EXCL);
|
|
2115
|
+
else throw Error('special file');
|
|
2116
|
+
}
|
|
2117
|
+
await copy(target,destination);
|
|
2118
|
+
}
|
|
2119
|
+
else throw Error('operation');
|
|
2120
|
+
})().catch(() => { process.exitCode = 1 });`;
|
|
2121
|
+
async function selectEngine(preference) {
|
|
2122
|
+
for (const engine of preference === "auto" ? ["podman", "docker"] : [preference]) {
|
|
2123
|
+
try {
|
|
2124
|
+
await execute(engine, ["info"], void 0, void 0, 15e3);
|
|
2125
|
+
return engine;
|
|
2126
|
+
} catch {
|
|
2127
|
+
if (preference !== "auto")
|
|
2128
|
+
throw new ACPFailure("runtime", { engine });
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
throw new ACPFailure("runtime");
|
|
2132
|
+
}
|
|
2133
|
+
async function openRuntime(workspace) {
|
|
2134
|
+
const environment = {};
|
|
2135
|
+
for (const [target, source] of Object.entries(workspace.environmentRefs)) {
|
|
2136
|
+
if (!process2.env[source])
|
|
2137
|
+
throw new ACPFailure("invalid", { environment: source });
|
|
2138
|
+
environment[target] = process2.env[source];
|
|
2139
|
+
}
|
|
2140
|
+
let root;
|
|
2141
|
+
let container;
|
|
2142
|
+
let engine;
|
|
2143
|
+
let closed = false;
|
|
2144
|
+
if (workspace.storage.type === "directory") {
|
|
2145
|
+
await mkdir(workspace.storage.path, { recursive: true });
|
|
2146
|
+
root = await realpath(resolve(workspace.storage.path));
|
|
2147
|
+
} else {
|
|
2148
|
+
engine = await selectEngine(workspace.storage.engine);
|
|
2149
|
+
root = "/workspace";
|
|
2150
|
+
container = await execute(engine, [
|
|
2151
|
+
"create",
|
|
2152
|
+
// 远程 Podman 会从服务端继承代理,绕过最小环境白名单;只允许显式 environmentRefs 注入。
|
|
2153
|
+
...engine === "podman" ? ["--http-proxy=false"] : [],
|
|
2154
|
+
"--init",
|
|
2155
|
+
"--mount",
|
|
2156
|
+
`type=volume,src=${workspace.storage.name},dst=/workspace`,
|
|
2157
|
+
"--mount",
|
|
2158
|
+
`type=volume,src=${workspace.storage.name}-state,dst=/state`,
|
|
2159
|
+
"--workdir",
|
|
2160
|
+
root,
|
|
2161
|
+
"--entrypoint",
|
|
2162
|
+
"node",
|
|
2163
|
+
workspace.storage.image,
|
|
2164
|
+
"-e",
|
|
2165
|
+
"setInterval(() => {}, 2147483647)"
|
|
2166
|
+
]);
|
|
2167
|
+
if (!/^[a-f0-9]{12,64}$/.test(container))
|
|
2168
|
+
throw new ACPFailure("runtime");
|
|
2169
|
+
try {
|
|
2170
|
+
await execute(engine, ["start", container]);
|
|
2171
|
+
} catch (error) {
|
|
2172
|
+
await execute(engine, ["rm", "-f", container]);
|
|
2173
|
+
throw error;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
const ensure = () => {
|
|
2177
|
+
if (closed)
|
|
2178
|
+
throw new ACPFailure("closed");
|
|
2179
|
+
};
|
|
2180
|
+
const path = (relative2) => workspace.storage.type === "volume" ? `${root}/${relative2}` : resolve(root, relative2);
|
|
2181
|
+
function transport(command, args, cwd, env = {}) {
|
|
2182
|
+
ensure();
|
|
2183
|
+
const values = { ...environment, ...env };
|
|
2184
|
+
return engine && container ? {
|
|
2185
|
+
type: "stdio",
|
|
2186
|
+
command: engine,
|
|
2187
|
+
args: [
|
|
2188
|
+
"exec",
|
|
2189
|
+
"-i",
|
|
2190
|
+
"--workdir",
|
|
2191
|
+
cwd,
|
|
2192
|
+
...Object.keys(values).flatMap((key) => [
|
|
2193
|
+
"--env",
|
|
2194
|
+
Object.hasOwn(engineEnvironment(), key) ? `${key}=${values[key]}` : key
|
|
2195
|
+
]),
|
|
2196
|
+
container,
|
|
2197
|
+
command,
|
|
2198
|
+
...args
|
|
2199
|
+
],
|
|
2200
|
+
env: { ...values, ...engineEnvironment() }
|
|
2201
|
+
} : { type: "stdio", command, args, cwd, env: values };
|
|
2202
|
+
}
|
|
2203
|
+
async function run(command, args, cwd = root, env) {
|
|
2204
|
+
const launch = transport(command, args, cwd, env);
|
|
2205
|
+
if (launch.type !== "stdio")
|
|
2206
|
+
throw new ACPFailure("runtime");
|
|
2207
|
+
return execute(launch.command, launch.args ?? [], launch.cwd, launch.env);
|
|
2208
|
+
}
|
|
2209
|
+
return {
|
|
2210
|
+
root,
|
|
2211
|
+
path,
|
|
2212
|
+
transport,
|
|
2213
|
+
run,
|
|
2214
|
+
resource: container ? { engine, container } : void 0,
|
|
2215
|
+
/**
|
|
2216
|
+
* 计算受管理的会话状态目录。
|
|
2217
|
+
* @param sessionId - 已校验的会话 ID。
|
|
2218
|
+
* @returns 状态目录绝对路径;不创建目录。
|
|
2219
|
+
* @example
|
|
2220
|
+
* ```ts
|
|
2221
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2222
|
+
* const state = runtime.state('task-1')
|
|
2223
|
+
* ```
|
|
2224
|
+
*/
|
|
2225
|
+
state: (sessionId) => workspace.storage.type === "volume" ? `/state/${sessionId}` : path(`.state/${sessionId}`),
|
|
2226
|
+
/**
|
|
2227
|
+
* 执行受工作区约束的文件操作,拒绝链接和路径逃逸。
|
|
2228
|
+
* @param op - mkdir/exists/write/read/copy 操作。
|
|
2229
|
+
* @param name - 工作区内相对路径或已校验的内部绝对路径。
|
|
2230
|
+
* @param content - write 时为 Base64,copy 时为目标路径,其余为空。
|
|
2231
|
+
* @returns 文本结果:exists 为 true/false,read 为 Base64,其余为空。
|
|
2232
|
+
* @example
|
|
2233
|
+
* ```ts
|
|
2234
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2235
|
+
* await runtime.file('mkdir', 'main/files')
|
|
2236
|
+
* ```
|
|
2237
|
+
*/
|
|
2238
|
+
async file(op, name, content = "") {
|
|
2239
|
+
return run(
|
|
2240
|
+
workspace.storage.type === "volume" ? "node" : process2.execPath,
|
|
2241
|
+
["-e", fileProgram, root, op, name, content]
|
|
2242
|
+
);
|
|
2243
|
+
},
|
|
2244
|
+
/**
|
|
2245
|
+
* 向已创建的独立状态目录写入配置,拒绝符号链接。
|
|
2246
|
+
* @param sessionId - 已校验的会话 ID。
|
|
2247
|
+
* @param name - 无目录分隔符的配置文件名。
|
|
2248
|
+
* @param content - 原生配置文本,可能含凭据,不应记录日志。
|
|
2249
|
+
* @returns 完成写入的 Promise。
|
|
2250
|
+
* @example
|
|
2251
|
+
* ```ts
|
|
2252
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2253
|
+
* await runtime.writeState('task-1', 'config.json', '{}')
|
|
2254
|
+
* ```
|
|
2255
|
+
*/
|
|
2256
|
+
async writeState(sessionId, name, content) {
|
|
2257
|
+
if (!/^[\w-]+\.[a-z0-9]+$/i.test(name))
|
|
2258
|
+
throw new ACPFailure("invalid");
|
|
2259
|
+
const directory = workspace.storage.type === "volume" ? `/state/${sessionId}` : path(`.state/${sessionId}`);
|
|
2260
|
+
await run(
|
|
2261
|
+
workspace.storage.type === "volume" ? "node" : process2.execPath,
|
|
2262
|
+
[
|
|
2263
|
+
"-e",
|
|
2264
|
+
'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})',
|
|
2265
|
+
directory,
|
|
2266
|
+
name,
|
|
2267
|
+
content
|
|
2268
|
+
]
|
|
2269
|
+
);
|
|
2270
|
+
},
|
|
2271
|
+
/**
|
|
2272
|
+
* 创建私有状态目录,拒绝祖先目录中的符号链接。
|
|
2273
|
+
* @param sessionId - 已校验的会话 ID。
|
|
2274
|
+
* @returns 已创建的状态目录绝对路径。
|
|
2275
|
+
* @example
|
|
2276
|
+
* ```ts
|
|
2277
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2278
|
+
* const directory = await runtime.stateDirectory('task-1')
|
|
2279
|
+
* ```
|
|
2280
|
+
*/
|
|
2281
|
+
async stateDirectory(sessionId) {
|
|
2282
|
+
const target = workspace.storage.type === "volume" ? `/state/${sessionId}` : path(`.state/${sessionId}`);
|
|
2283
|
+
await run(
|
|
2284
|
+
workspace.storage.type === "volume" ? "node" : process2.execPath,
|
|
2285
|
+
[
|
|
2286
|
+
"-e",
|
|
2287
|
+
'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})}}',
|
|
2288
|
+
target
|
|
2289
|
+
]
|
|
2290
|
+
);
|
|
2291
|
+
return target;
|
|
2292
|
+
},
|
|
2293
|
+
/**
|
|
2294
|
+
* 关闭本次拥有的容器;目录和 volume 保留。
|
|
2295
|
+
* @returns 清理完成的 Promise;失败抛错,不能视为资源已释放。
|
|
2296
|
+
* @example
|
|
2297
|
+
* ```ts
|
|
2298
|
+
* // 内部上下文:runtime/git/connection 为相应工厂已创建的句柄。
|
|
2299
|
+
* await runtime.close()
|
|
2300
|
+
* ```
|
|
2301
|
+
*/
|
|
2302
|
+
async close() {
|
|
2303
|
+
if (closed)
|
|
2304
|
+
return;
|
|
2305
|
+
if (engine && container) {
|
|
2306
|
+
try {
|
|
2307
|
+
await execute(engine, ["rm", "--force", container]);
|
|
2308
|
+
} catch {
|
|
2309
|
+
throw new ACPFailure("cleanup", { engine, container });
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
closed = true;
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// src/acp/ai-acp-functions.ts
|
|
2318
|
+
function createACPOperations(provider, config, extensions = {}, modelConfig = async () => config, mcpLookup) {
|
|
2319
|
+
const workspaces = provider.createRelStore(
|
|
2320
|
+
"hai_ai_acp_workspaces"
|
|
2321
|
+
);
|
|
2322
|
+
const agents = provider.createRelStore("hai_ai_acp_agents");
|
|
2323
|
+
const sessions = provider.createRelStore(
|
|
2324
|
+
"hai_ai_acp_sessions"
|
|
2325
|
+
);
|
|
2326
|
+
const locks = provider.createRelStore(
|
|
2327
|
+
"hai_ai_acp_locks"
|
|
2328
|
+
);
|
|
2329
|
+
const adapters = { ...nativeAdapters, ...extensions };
|
|
2330
|
+
const live = /* @__PURE__ */ new Map();
|
|
2331
|
+
const connections = /* @__PURE__ */ new Set();
|
|
2332
|
+
const limits = ACPConfigSchema.parse(config.acp ?? {});
|
|
2333
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2334
|
+
const stopping = /* @__PURE__ */ new Map();
|
|
2335
|
+
let closed = false;
|
|
2336
|
+
const agentRegistry = createResourceRegistry(
|
|
2337
|
+
agents,
|
|
2338
|
+
ACPAgentSchema,
|
|
2339
|
+
config.acp?.agents ?? [],
|
|
2340
|
+
() => !closed
|
|
2341
|
+
);
|
|
2342
|
+
const workspaceRegistry = createResourceRegistry(
|
|
2343
|
+
workspaces,
|
|
2344
|
+
ACPWorkspaceSchema,
|
|
2345
|
+
config.acp?.workspaces ?? [],
|
|
2346
|
+
() => !closed
|
|
2347
|
+
);
|
|
2348
|
+
function ensure() {
|
|
2349
|
+
if (closed)
|
|
2350
|
+
throw new ACPFailure("closed");
|
|
2351
|
+
}
|
|
2352
|
+
async function protect(operation) {
|
|
2353
|
+
const task = (async () => {
|
|
2354
|
+
try {
|
|
2355
|
+
ensure();
|
|
2356
|
+
return ok(await operation());
|
|
2357
|
+
} catch (error) {
|
|
2358
|
+
return acpError(error);
|
|
2359
|
+
}
|
|
2360
|
+
})();
|
|
2361
|
+
pending.add(task);
|
|
2362
|
+
try {
|
|
2363
|
+
return await task;
|
|
2364
|
+
} finally {
|
|
2365
|
+
pending.delete(task);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
async function modelSignature(agent) {
|
|
2369
|
+
return createHash("sha256").update(
|
|
2370
|
+
JSON.stringify(
|
|
2371
|
+
resolveAgentModels(agent, await modelConfig()).map(
|
|
2372
|
+
({ id, config: model }) => ({
|
|
2373
|
+
id,
|
|
2374
|
+
model: model.model,
|
|
2375
|
+
protocol: model.protocol,
|
|
2376
|
+
baseUrl: model.baseUrl,
|
|
2377
|
+
capabilities: model.capabilities
|
|
2378
|
+
})
|
|
2379
|
+
)
|
|
2380
|
+
)
|
|
2381
|
+
).digest("hex");
|
|
2382
|
+
}
|
|
2383
|
+
async function protectResult(operation) {
|
|
2384
|
+
const result = await protect(operation);
|
|
2385
|
+
return result.success ? result.data : result;
|
|
2386
|
+
}
|
|
2387
|
+
async function getAgent(id) {
|
|
2388
|
+
const entry = await agentRegistry.read(id);
|
|
2389
|
+
if (!entry)
|
|
2390
|
+
throw new ACPFailure("missing", { id });
|
|
2391
|
+
return entry.value;
|
|
2392
|
+
}
|
|
2393
|
+
async function getWorkspace(id) {
|
|
2394
|
+
const entry = await workspaceRegistry.read(id);
|
|
2395
|
+
if (!entry)
|
|
2396
|
+
throw new ACPFailure("missing", { id });
|
|
2397
|
+
const workspace = await normalizeWorkspace(entry.value);
|
|
2398
|
+
await checkWorkspaceOverlap(workspace);
|
|
2399
|
+
return workspace;
|
|
2400
|
+
}
|
|
2401
|
+
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}`;
|
|
2402
|
+
async function normalizeWorkspace(workspace) {
|
|
2403
|
+
if (workspace.storage.type === "directory") {
|
|
2404
|
+
await mkdir(workspace.storage.path, { recursive: true });
|
|
2405
|
+
workspace.storage.path = await realpath(resolve(workspace.storage.path));
|
|
2406
|
+
} else {
|
|
2407
|
+
workspace.storage.engine = await selectEngine(workspace.storage.engine);
|
|
2408
|
+
}
|
|
2409
|
+
return workspace;
|
|
2410
|
+
}
|
|
2411
|
+
async function canonicalDirectory(path) {
|
|
2412
|
+
const target = resolve(path);
|
|
2413
|
+
try {
|
|
2414
|
+
return await realpath(target);
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
if (error.code !== "ENOENT" || dirname(target) === target) {
|
|
2417
|
+
throw error;
|
|
2418
|
+
}
|
|
2419
|
+
return resolve(
|
|
2420
|
+
await canonicalDirectory(dirname(target)),
|
|
2421
|
+
basename(target)
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
async function checkWorkspaceOverlap(workspace) {
|
|
2426
|
+
for (const entry of await workspaceRegistry.values()) {
|
|
2427
|
+
if (entry.id === workspace.id)
|
|
2428
|
+
continue;
|
|
2429
|
+
if (entry.storage.type === "directory" && workspace.storage.type === "directory") {
|
|
2430
|
+
const existing = await canonicalDirectory(entry.storage.path);
|
|
2431
|
+
const child = relative(existing, workspace.storage.path);
|
|
2432
|
+
const parent = relative(workspace.storage.path, existing);
|
|
2433
|
+
if (!child.startsWith("..") && !isAbsolute(child) || !parent.startsWith("..") && !isAbsolute(parent)) {
|
|
2434
|
+
throw new ACPFailure("conflict");
|
|
2435
|
+
}
|
|
2436
|
+
} 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)) {
|
|
2437
|
+
throw new ACPFailure("conflict");
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
async function get(store, id) {
|
|
2442
|
+
sessionIdentitySchema.parse(id);
|
|
2443
|
+
const data = await store.get(id);
|
|
2444
|
+
if (!data)
|
|
2445
|
+
throw new ACPFailure("missing", { id });
|
|
2446
|
+
return data;
|
|
2447
|
+
}
|
|
2448
|
+
async function acquire(key) {
|
|
2449
|
+
const previous = await locks.readVersioned(key);
|
|
2450
|
+
if (previous?.data.owner)
|
|
2451
|
+
throw new ACPFailure("busy");
|
|
2452
|
+
const owner = randomUUID();
|
|
2453
|
+
const version = await locks.compareAndSwap(key, {
|
|
2454
|
+
expectedVersion: previous?.version,
|
|
2455
|
+
data: { owner }
|
|
2456
|
+
});
|
|
2457
|
+
if (!version)
|
|
2458
|
+
throw new ACPFailure("busy");
|
|
2459
|
+
return async () => {
|
|
2460
|
+
if (!await locks.compareAndSwap(key, {
|
|
2461
|
+
expectedVersion: version,
|
|
2462
|
+
data: { owner: null }
|
|
2463
|
+
})) {
|
|
2464
|
+
throw new ACPFailure("cleanup");
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
async function withDefinitions(operation) {
|
|
2469
|
+
const release = await acquire("definition-registry");
|
|
2470
|
+
try {
|
|
2471
|
+
return await operation();
|
|
2472
|
+
} finally {
|
|
2473
|
+
await release();
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
async function workspaceOperation(workspace, action) {
|
|
2477
|
+
const release = await acquire(`${workspaceKey(workspace)}:main`);
|
|
2478
|
+
let runtime;
|
|
2479
|
+
try {
|
|
2480
|
+
runtime = await openRuntime(workspace);
|
|
2481
|
+
return await action(runtime);
|
|
2482
|
+
} finally {
|
|
2483
|
+
await runtime?.close();
|
|
2484
|
+
await release();
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
async function unique(store, id, data) {
|
|
2488
|
+
if (!await store.compareAndSwap(id, { expectedVersion: void 0, data }))
|
|
2489
|
+
throw new ACPFailure("conflict", { id });
|
|
2490
|
+
return data;
|
|
2491
|
+
}
|
|
2492
|
+
async function validateAgent(agent) {
|
|
2493
|
+
if (agent.mode === "custom-service")
|
|
2494
|
+
return;
|
|
2495
|
+
if (!adapters[agent.adapter])
|
|
2496
|
+
throw new ACPFailure("unsupported", { adapter: agent.adapter });
|
|
2497
|
+
resolveAgentModels(agent, await modelConfig());
|
|
2498
|
+
}
|
|
2499
|
+
async function launchAgent(agent, runtime, info) {
|
|
2500
|
+
if (agent.mode === "custom-service") {
|
|
2501
|
+
return {
|
|
2502
|
+
type: agent.transport,
|
|
2503
|
+
url: agent.url,
|
|
2504
|
+
headers: resolveEnvironment(agent.headerEnv)
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
if (!runtime)
|
|
2508
|
+
throw new ACPFailure("missing");
|
|
2509
|
+
const statePath = await runtime.stateDirectory(info.id);
|
|
2510
|
+
const native = await adapters[agent.adapter].configure({
|
|
2511
|
+
agent,
|
|
2512
|
+
models: resolveAgentModels(agent, await modelConfig()),
|
|
2513
|
+
modelId: info.modelId,
|
|
2514
|
+
statePath
|
|
2515
|
+
});
|
|
2516
|
+
for (const [name, content] of Object.entries(native.files ?? {}))
|
|
2517
|
+
await runtime.writeState(info.id, name, content);
|
|
2518
|
+
return runtime.transport(native.command, native.args, info.cwd, {
|
|
2519
|
+
HOME: statePath,
|
|
2520
|
+
USERPROFILE: statePath,
|
|
2521
|
+
...resolveEnvironment(agent.environmentRefs),
|
|
2522
|
+
...native.env
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
async function stop(id) {
|
|
2526
|
+
const existing = stopping.get(id);
|
|
2527
|
+
if (existing)
|
|
2528
|
+
return existing;
|
|
2529
|
+
const task = stopOne(id);
|
|
2530
|
+
stopping.set(id, task);
|
|
2531
|
+
try {
|
|
2532
|
+
await task;
|
|
2533
|
+
} finally {
|
|
2534
|
+
stopping.delete(id);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
async function stopOne(id) {
|
|
2538
|
+
const current = live.get(id);
|
|
2539
|
+
if (!current)
|
|
2540
|
+
return;
|
|
2541
|
+
await current.connection.cancel();
|
|
2542
|
+
try {
|
|
2543
|
+
await current.connection.closeSession();
|
|
2544
|
+
} catch {
|
|
2545
|
+
} finally {
|
|
2546
|
+
await current.connection.close();
|
|
2547
|
+
await current.runtime?.close();
|
|
2548
|
+
}
|
|
2549
|
+
current.info.status = "closed";
|
|
2550
|
+
current.info.resource = void 0;
|
|
2551
|
+
await sessions.save(id, current.info);
|
|
2552
|
+
await current.release();
|
|
2553
|
+
live.delete(id);
|
|
2554
|
+
}
|
|
2555
|
+
async function start(info, options, resume) {
|
|
2556
|
+
const agent = info.agent;
|
|
2557
|
+
const releaseSession = await acquire(`session:${info.id}`);
|
|
2558
|
+
let releaseDirectory;
|
|
2559
|
+
let runtime;
|
|
2560
|
+
let connection;
|
|
2561
|
+
let recordExists = true;
|
|
2562
|
+
try {
|
|
2563
|
+
const workspace = info.workspaceId ? await getWorkspace(info.workspaceId) : void 0;
|
|
2564
|
+
if (!agent.enabled)
|
|
2565
|
+
throw new ACPFailure("unsupported");
|
|
2566
|
+
if (resume && !await sessions.get(info.id)) {
|
|
2567
|
+
recordExists = false;
|
|
2568
|
+
throw new ACPFailure("missing");
|
|
2569
|
+
}
|
|
2570
|
+
if (workspace) {
|
|
2571
|
+
if (agent.mode === "custom-service")
|
|
2572
|
+
throw new ACPFailure("unsupported");
|
|
2573
|
+
if (agent.mode === "embedded" && workspace.storage.type !== "directory" || agent.mode === "builtin-service" && workspace.storage.type !== "volume") {
|
|
2574
|
+
throw new ACPFailure("unsupported");
|
|
2575
|
+
}
|
|
2576
|
+
if (agent.mode === "builtin-service" && workspace.storage.type === "volume" && (agent.image !== workspace.storage.image || agent.engine !== "auto" && agent.engine !== workspace.storage.engine)) {
|
|
2577
|
+
throw new ACPFailure("conflict");
|
|
2578
|
+
}
|
|
2579
|
+
if (!info.newWorktree)
|
|
2580
|
+
releaseDirectory = await acquire(`${workspaceKey(workspace)}:main`);
|
|
2581
|
+
if (!resume) {
|
|
2582
|
+
const releasePrepare = info.newWorktree ? await acquire(`${workspaceKey(workspace)}:main`) : void 0;
|
|
2583
|
+
try {
|
|
2584
|
+
runtime = await openRuntime(workspace);
|
|
2585
|
+
info.resource = runtime.resource;
|
|
2586
|
+
await sessions.save(info.id, info);
|
|
2587
|
+
const git = createGitOperations(workspace, runtime);
|
|
2588
|
+
await git.prepare();
|
|
2589
|
+
info.cwd = info.newWorktree ? await git.createWorktree(info.id, info.branch) : runtime.path("main");
|
|
2590
|
+
} finally {
|
|
2591
|
+
await releasePrepare?.();
|
|
2592
|
+
}
|
|
2593
|
+
} else {
|
|
2594
|
+
runtime = await openRuntime(workspace);
|
|
2595
|
+
}
|
|
2596
|
+
} else if (agent.mode !== "custom-service" || !info.cwd || info.newWorktree) {
|
|
2597
|
+
throw new ACPFailure("invalid");
|
|
2598
|
+
}
|
|
2599
|
+
ensure();
|
|
2600
|
+
const transport = await launchAgent(agent, runtime, info);
|
|
2601
|
+
connection = await connectACP(transport, options, limits);
|
|
2602
|
+
if (info.mcpServerIds?.length && !mcpLookup)
|
|
2603
|
+
throw new ACPFailure("unsupported");
|
|
2604
|
+
const mcpServers = [
|
|
2605
|
+
...info.mcpServers,
|
|
2606
|
+
...info.mcpServerIds?.length && mcpLookup ? await mcpLookup(info.mcpServerIds) : []
|
|
2607
|
+
];
|
|
2608
|
+
if (resume) {
|
|
2609
|
+
if (!info.nativeId)
|
|
2610
|
+
throw new ACPFailure("unsupported");
|
|
2611
|
+
await connection.resumeSession(info.nativeId, info.cwd, mcpServers);
|
|
2612
|
+
} else {
|
|
2613
|
+
info.nativeId = (await connection.createSession(info.cwd, mcpServers)).sessionId;
|
|
2614
|
+
}
|
|
2615
|
+
ensure();
|
|
2616
|
+
info.status = "active";
|
|
2617
|
+
info.resource = runtime?.resource;
|
|
2618
|
+
await sessions.save(info.id, info);
|
|
2619
|
+
const value = {
|
|
2620
|
+
info,
|
|
2621
|
+
connection,
|
|
2622
|
+
runtime,
|
|
2623
|
+
release: async () => {
|
|
2624
|
+
await releaseDirectory?.();
|
|
2625
|
+
await releaseSession();
|
|
2626
|
+
}
|
|
2627
|
+
};
|
|
2628
|
+
live.set(info.id, value);
|
|
2629
|
+
return handle(info.id, value);
|
|
2630
|
+
} catch (error) {
|
|
2631
|
+
await connection?.close();
|
|
2632
|
+
await runtime?.close();
|
|
2633
|
+
info.status = "failed";
|
|
2634
|
+
info.resource = void 0;
|
|
2635
|
+
if (recordExists)
|
|
2636
|
+
await sessions.save(info.id, info);
|
|
2637
|
+
await releaseDirectory?.();
|
|
2638
|
+
await releaseSession();
|
|
2639
|
+
throw error;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
function handle(id, expected) {
|
|
2643
|
+
function current() {
|
|
2644
|
+
ensure();
|
|
2645
|
+
const value = live.get(id);
|
|
2646
|
+
if (!value || expected && value !== expected)
|
|
2647
|
+
throw new ACPFailure("closed");
|
|
2648
|
+
return value;
|
|
2649
|
+
}
|
|
2650
|
+
async function gitOperation(publish) {
|
|
2651
|
+
const request = publish ? ACPPublishSchema.parse(publish) : void 0;
|
|
2652
|
+
const info = await get(sessions, id);
|
|
2653
|
+
if (!info.workspaceId || request && !info.newWorktree)
|
|
2654
|
+
throw new ACPFailure("unsupported");
|
|
2655
|
+
const workspace = await getWorkspace(info.workspaceId);
|
|
2656
|
+
await stop(id);
|
|
2657
|
+
const release = await acquire(`session:${id}`);
|
|
2658
|
+
try {
|
|
2659
|
+
return await workspaceOperation(workspace, (runtime) => request ? createGitOperations(workspace, runtime).publish(
|
|
2660
|
+
id,
|
|
2661
|
+
info.branch,
|
|
2662
|
+
request
|
|
2663
|
+
) : createGitOperations(workspace, runtime).updateMain());
|
|
2664
|
+
} finally {
|
|
2665
|
+
await release();
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
return {
|
|
2669
|
+
id,
|
|
2670
|
+
/**
|
|
2671
|
+
* 读取持久会话信息,包括状态、原生 ID 和当前容器。
|
|
2672
|
+
* @returns 包含 ACPSessionInfo 的 HaiResult;记录不存在时返回失败。
|
|
2673
|
+
* @example
|
|
2674
|
+
* ```ts
|
|
2675
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2676
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2677
|
+
* const result = await session.info()
|
|
2678
|
+
* ```
|
|
2679
|
+
*/
|
|
2680
|
+
info: () => protect(() => get(sessions, id)),
|
|
2681
|
+
/**
|
|
2682
|
+
* 发送一轮内容,等待最终停止原因、最终消息文本和完整事件;同一会话不允许并发轮次。
|
|
2683
|
+
* @param content - 原生 ACP 内容块,不允许同一会话并发执行。
|
|
2684
|
+
* @param signal - 本轮取消信号;取消超时会关闭连接。
|
|
2685
|
+
* @returns 最终文本、停止原因和事件的 HaiResult。
|
|
2686
|
+
* @example
|
|
2687
|
+
* ```ts
|
|
2688
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2689
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2690
|
+
* const result = await session.prompt([{ type: 'text', text: '检查工程' }])
|
|
2691
|
+
* ```
|
|
2692
|
+
*/
|
|
2693
|
+
prompt: (content, signal) => protect(() => current().connection.prompt(content, signal)),
|
|
2694
|
+
/**
|
|
2695
|
+
* 仅接受正常结束的一份 JSON(允许完整 JSON 代码围栏),再按 schema 校验;前后夹杂文本视为失败。
|
|
2696
|
+
* @param content - 要求返回结构化 JSON 的原生内容块。
|
|
2697
|
+
* @param schema - 最终 JSON 的 Zod 校验器。
|
|
2698
|
+
* @param signal - 可选的本轮取消信号。
|
|
2699
|
+
* @returns 经过 schema 校验的数据;输出不合法时返回 ACP_OUTPUT_INVALID。
|
|
2700
|
+
* @example
|
|
2701
|
+
* ```ts
|
|
2702
|
+
* import { z } from 'zod'
|
|
2703
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2704
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2705
|
+
* const result = await session.promptStructured([{ type: 'text', text: '返回 JSON' }], z.object({ ok: z.boolean() }))
|
|
2706
|
+
* ```
|
|
2707
|
+
*/
|
|
2708
|
+
promptStructured: (content, schema, signal) => protect(async () => {
|
|
2709
|
+
const response = await current().connection.prompt(content, signal);
|
|
2710
|
+
if (response.stopReason !== "end_turn")
|
|
2711
|
+
throw new ACPFailure("output");
|
|
2712
|
+
let text = response.text.trim();
|
|
2713
|
+
const fenced = /^```(?:json)?[ \t]*\n([\s\S]*?)\n```$/.exec(text);
|
|
2714
|
+
if (fenced)
|
|
2715
|
+
text = fenced[1];
|
|
2716
|
+
try {
|
|
2717
|
+
return schema.parse(JSON.parse(text));
|
|
2718
|
+
} catch {
|
|
2719
|
+
throw new ACPFailure("output");
|
|
2720
|
+
}
|
|
2721
|
+
}),
|
|
2722
|
+
/**
|
|
2723
|
+
* 迭代本轮 ACP 事件;中途退出迭代会取消本轮,失败会抛错,调用方需 try/finally 清理。
|
|
2724
|
+
* @param content - 本轮原生内容块。
|
|
2725
|
+
* @param signal - 可选取消信号;结束迭代同样会取消本轮。
|
|
2726
|
+
* @returns 异步事件迭代器;流式失败抛出异常,需在调用方捕获。
|
|
2727
|
+
* @throws 连接关闭、取消超时、输出超限或原生请求失败。
|
|
2728
|
+
* @example
|
|
2729
|
+
* ```ts
|
|
2730
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2731
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2732
|
+
* for await (const event of session.stream([{ type: 'text', text: '检查工程' }])) { const update = event.update }
|
|
2733
|
+
* ```
|
|
2734
|
+
*/
|
|
2735
|
+
async *stream(content, signal) {
|
|
2736
|
+
const value = current();
|
|
2737
|
+
const controller = new AbortController();
|
|
2738
|
+
const queue = [];
|
|
2739
|
+
let wake;
|
|
2740
|
+
const completion = { done: false };
|
|
2741
|
+
let error;
|
|
2742
|
+
const unsubscribe = value.connection.subscribe((event) => {
|
|
2743
|
+
if (queue.length >= limits.maxPendingEvents)
|
|
2744
|
+
throw new ACPFailure("output");
|
|
2745
|
+
queue.push(event);
|
|
2746
|
+
wake?.();
|
|
2747
|
+
});
|
|
2748
|
+
const task = value.connection.prompt(
|
|
2749
|
+
content,
|
|
2750
|
+
signal ? AbortSignal.any([signal, controller.signal]) : controller.signal
|
|
2751
|
+
).catch((reason) => {
|
|
2752
|
+
error = reason;
|
|
2753
|
+
}).finally(() => {
|
|
2754
|
+
completion.done = true;
|
|
2755
|
+
wake?.();
|
|
2756
|
+
});
|
|
2757
|
+
try {
|
|
2758
|
+
while (!completion.done || queue.length) {
|
|
2759
|
+
if (queue.length) {
|
|
2760
|
+
yield queue.shift();
|
|
2761
|
+
} else {
|
|
2762
|
+
await new Promise((resolve3) => {
|
|
2763
|
+
wake = resolve3;
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
if (error)
|
|
2768
|
+
throw error;
|
|
2769
|
+
} finally {
|
|
2770
|
+
unsubscribe();
|
|
2771
|
+
controller.abort();
|
|
2772
|
+
await task;
|
|
2773
|
+
}
|
|
2774
|
+
},
|
|
2775
|
+
/**
|
|
2776
|
+
* 请求取消正在执行的一轮;超时未停止时关闭所拥有的传输。
|
|
2777
|
+
* @returns 取消完成的 HaiResult;超时或清理失败返回错误。
|
|
2778
|
+
* @example
|
|
2779
|
+
* ```ts
|
|
2780
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2781
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2782
|
+
* const result = await session.cancel()
|
|
2783
|
+
* ```
|
|
2784
|
+
*/
|
|
2785
|
+
cancel: () => protect(() => current().connection.cancel()),
|
|
2786
|
+
/**
|
|
2787
|
+
* 选择智能体公布的原生模式 ID;不兼容时返回协议失败。
|
|
2788
|
+
* @param mode - 握手或事件中公布的原生模式 ID。
|
|
2789
|
+
* @returns 原生模式设置结果的 HaiResult。
|
|
2790
|
+
* @example
|
|
2791
|
+
* ```ts
|
|
2792
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2793
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2794
|
+
* const result = await session.setMode('code')
|
|
2795
|
+
* ```
|
|
2796
|
+
*/
|
|
2797
|
+
setMode: (mode) => protect(() => current().connection.setMode(mode)),
|
|
2798
|
+
/**
|
|
2799
|
+
* 设置智能体公布的原生配置选项;值必须符合该智能体的选项定义。
|
|
2800
|
+
* @param key - 原生配置选项 ID。
|
|
2801
|
+
* @param value - 该选项允许的值。
|
|
2802
|
+
* @returns 原生配置更新结果的 HaiResult。
|
|
2803
|
+
* @example
|
|
2804
|
+
* ```ts
|
|
2805
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2806
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2807
|
+
* const result = await session.setConfigOption('model', 'coding')
|
|
2808
|
+
* ```
|
|
2809
|
+
*/
|
|
2810
|
+
setConfigOption: (key, value) => protect(() => current().connection.setConfigOption(key, value)),
|
|
2811
|
+
/**
|
|
2812
|
+
* 先关闭会话执行环境,再 fetch 并仅快进各工程明确配置的主分支;不修改会话分支。
|
|
2813
|
+
* @returns 各仓更新报告;部分失败时报告位于 error.cause.reports。
|
|
2814
|
+
* @example
|
|
2815
|
+
* ```ts
|
|
2816
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2817
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2818
|
+
* const result = await session.updateMain()
|
|
2819
|
+
* ```
|
|
2820
|
+
*/
|
|
2821
|
+
updateMain: () => protect(() => gitOperation()),
|
|
2822
|
+
/**
|
|
2823
|
+
* 关闭执行环境后逐仓提交并推送;仅支持受管理的独立会话,部分失败保留各仓报告,禁止强推。
|
|
2824
|
+
* @param request - 发布模式、提交说明和作者;仅支持受管理的独立会话。
|
|
2825
|
+
* @returns 各仓提交/推送报告;部分失败时报告位于 error.cause.reports。
|
|
2826
|
+
* @example
|
|
2827
|
+
* ```ts
|
|
2828
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2829
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2830
|
+
* const result = await session.publish({ mode: 'merge-main', message: '完成任务', author: { name: 'Developer', email: 'dev@example.com' } })
|
|
2831
|
+
* ```
|
|
2832
|
+
*/
|
|
2833
|
+
publish: (request) => protect(() => gitOperation(request)),
|
|
2834
|
+
/**
|
|
2835
|
+
* 停止拥有的进程或容器,释放占用;保留工作目录和原生状态,供后续显式恢复。
|
|
2836
|
+
* @returns 关闭完成的 HaiResult;清理失败时不释放仍在占用的资源。
|
|
2837
|
+
* @example
|
|
2838
|
+
* ```ts
|
|
2839
|
+
* // session 为 sessions.create/get/resume 成功返回的 data。
|
|
2840
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2841
|
+
* const result = await session.close()
|
|
2842
|
+
* ```
|
|
2843
|
+
*/
|
|
2844
|
+
close: () => protect(() => stop(id))
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
return {
|
|
2848
|
+
agents: {
|
|
2849
|
+
/**
|
|
2850
|
+
* 校验模型与适配器后新增数据库智能体;配置同名条目只读。
|
|
2851
|
+
* @param input - 完整智能体定义,必须引用已有 Hai 模型 ID。
|
|
2852
|
+
* @returns 带 source/version 的新增条目;配置同 ID 返回只读错误。
|
|
2853
|
+
* @example
|
|
2854
|
+
* ```ts
|
|
2855
|
+
* import { ai } from '@h-ai/ai'
|
|
2856
|
+
* // 前提:ai 已成功初始化。
|
|
2857
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2858
|
+
* const result = await ai.acp.agents.register({ id: 'coder', name: 'Coder', mode: 'embedded', adapter: 'codex', modelIds: ['coding'] })
|
|
2859
|
+
* ```
|
|
2860
|
+
*/
|
|
2861
|
+
register: (input) => protectResult(async () => {
|
|
2862
|
+
const agent = ACPAgentSchema.parse(input);
|
|
2863
|
+
await validateAgent(agent);
|
|
2864
|
+
return agentRegistry.operations.register(agent);
|
|
2865
|
+
}),
|
|
2866
|
+
/**
|
|
2867
|
+
* 按 ID 读取合并清单条目;source 区分配置与数据库,数据库 get 返回 version。
|
|
2868
|
+
* @param id - 智能体 ID。
|
|
2869
|
+
* @returns 合并后的资源条目;未找到时返回资源不存在错误。
|
|
2870
|
+
* @example
|
|
2871
|
+
* ```ts
|
|
2872
|
+
* import { ai } from '@h-ai/ai'
|
|
2873
|
+
* // 前提:ai 已成功初始化。
|
|
2874
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2875
|
+
* const result = await ai.acp.agents.get('coder')
|
|
2876
|
+
* ```
|
|
2877
|
+
*/
|
|
2878
|
+
get: agentRegistry.operations.get,
|
|
2879
|
+
/**
|
|
2880
|
+
* 列出配置与数据库合并后的智能体;同 ID 配置优先。
|
|
2881
|
+
* @returns 去重后的智能体条目列表;列表项不带数据库 version。
|
|
2882
|
+
* @example
|
|
2883
|
+
* ```ts
|
|
2884
|
+
* import { ai } from '@h-ai/ai'
|
|
2885
|
+
* // 前提:ai 已成功初始化。
|
|
2886
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2887
|
+
* const result = await ai.acp.agents.list()
|
|
2888
|
+
* ```
|
|
2889
|
+
*/
|
|
2890
|
+
list: agentRegistry.operations.list,
|
|
2891
|
+
/**
|
|
2892
|
+
* 整体替换数据库智能体;可传入 get 返回的版本防止覆盖并发修改,配置条目不可改。
|
|
2893
|
+
* @param input - 包含原 ID 的完整替换定义。
|
|
2894
|
+
* @param version - get 返回的数据库版本;传入时校验并发修改。
|
|
2895
|
+
* @returns 更新后的资源条目;配置只读、版本冲突均返回失败。
|
|
2896
|
+
* @example
|
|
2897
|
+
* ```ts
|
|
2898
|
+
* import { ai } from '@h-ai/ai'
|
|
2899
|
+
* // 前提:ai 已成功初始化。
|
|
2900
|
+
* // definition 为完整替换定义;entry 为 get 成功返回的条目。
|
|
2901
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2902
|
+
* const result = await ai.acp.agents.update(definition, entry.version)
|
|
2903
|
+
* ```
|
|
2904
|
+
*/
|
|
2905
|
+
update: (input, version) => protectResult(async () => {
|
|
2906
|
+
const agent = ACPAgentSchema.parse(input);
|
|
2907
|
+
await validateAgent(agent);
|
|
2908
|
+
return agentRegistry.operations.update(agent, version);
|
|
2909
|
+
}),
|
|
2910
|
+
/**
|
|
2911
|
+
* 删除无会话引用的数据库智能体定义;不删除镜像或外部服务。
|
|
2912
|
+
* @param id - 数据库智能体 ID。
|
|
2913
|
+
* @param version - 可选的期望版本。
|
|
2914
|
+
* @returns 删除完成的 HaiResult;存在会话引用时返回 ACP_BUSY。
|
|
2915
|
+
* @example
|
|
2916
|
+
* ```ts
|
|
2917
|
+
* import { ai } from '@h-ai/ai'
|
|
2918
|
+
* // 前提:ai 已成功初始化。
|
|
2919
|
+
* // entry 为 get 成功返回的数据库条目。
|
|
2920
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2921
|
+
* const result = await ai.acp.agents.remove('coder', entry.version)
|
|
2922
|
+
* ```
|
|
2923
|
+
*/
|
|
2924
|
+
remove: (id, version) => protectResult(async () => {
|
|
2925
|
+
return withDefinitions(async () => {
|
|
2926
|
+
const existing = await agentRegistry.read(id);
|
|
2927
|
+
if (existing?.source === "config")
|
|
2928
|
+
return agentRegistry.operations.remove(id, version);
|
|
2929
|
+
if ((await sessions.query({ where: { agentId: id } })).length)
|
|
2930
|
+
throw new ACPFailure("busy");
|
|
2931
|
+
return agentRegistry.operations.remove(id, version);
|
|
2932
|
+
});
|
|
2933
|
+
}),
|
|
2934
|
+
/**
|
|
2935
|
+
* 解析模型和原生配置,验证适配器是否兼容;不持久保存模型凭据。
|
|
2936
|
+
* @param id - 已登记智能体 ID。
|
|
2937
|
+
* @param modelId - 可选模型 ID,须在智能体 modelIds 中。
|
|
2938
|
+
* @returns 智能体模式与绑定模型摘要;不返回凭据或原生配置内容。
|
|
2939
|
+
* @example
|
|
2940
|
+
* ```ts
|
|
2941
|
+
* import { ai } from '@h-ai/ai'
|
|
2942
|
+
* // 前提:ai 已成功初始化。
|
|
2943
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2944
|
+
* const result = await ai.acp.agents.initialize('coder')
|
|
2945
|
+
* ```
|
|
2946
|
+
*/
|
|
2947
|
+
initialize: (id, modelId) => protect(async () => {
|
|
2948
|
+
const agent = await getAgent(id);
|
|
2949
|
+
await validateAgent(agent);
|
|
2950
|
+
if (agent.mode === "custom-service")
|
|
2951
|
+
return { agentId: id, mode: agent.mode, modelIds: [] };
|
|
2952
|
+
await adapters[agent.adapter].configure({
|
|
2953
|
+
agent,
|
|
2954
|
+
models: resolveAgentModels(agent, await modelConfig()),
|
|
2955
|
+
modelId: modelId ?? agent.defaultModelId ?? agent.modelIds[0],
|
|
2956
|
+
statePath: "/state/validation"
|
|
2957
|
+
});
|
|
2958
|
+
return { agentId: id, mode: agent.mode, modelIds: agent.modelIds };
|
|
2959
|
+
}),
|
|
2960
|
+
/**
|
|
2961
|
+
* 实际启动并握手后关闭连接,返回智能体能力;托管模式需指定工作区。
|
|
2962
|
+
* @param id - 已登记智能体 ID。
|
|
2963
|
+
* @param options - 回调、能力及工作区;托管模式必须传 workspaceId。
|
|
2964
|
+
* @returns 原生初始化结果(智能体能力、认证方法等)的 HaiResult。
|
|
2965
|
+
* @example
|
|
2966
|
+
* ```ts
|
|
2967
|
+
* import { ai } from '@h-ai/ai'
|
|
2968
|
+
* // 前提:ai 已成功初始化。
|
|
2969
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
2970
|
+
* const result = await ai.acp.agents.check('coder', { workspaceId: 'project' })
|
|
2971
|
+
* ```
|
|
2972
|
+
*/
|
|
2973
|
+
check: (id, options = {}) => protect(async () => {
|
|
2974
|
+
const agent = await getAgent(id);
|
|
2975
|
+
await validateAgent(agent);
|
|
2976
|
+
if (agent.mode !== "custom-service") {
|
|
2977
|
+
if (!options.workspaceId)
|
|
2978
|
+
throw new ACPFailure("invalid");
|
|
2979
|
+
const workspace = await getWorkspace(options.workspaceId);
|
|
2980
|
+
if (agent.mode === "embedded" && workspace.storage.type !== "directory" || agent.mode === "builtin-service" && (workspace.storage.type !== "volume" || workspace.storage.image !== agent.image)) {
|
|
2981
|
+
throw new ACPFailure("unsupported");
|
|
2982
|
+
}
|
|
2983
|
+
return workspaceOperation(workspace, async (runtime) => {
|
|
2984
|
+
const info = {
|
|
2985
|
+
id: randomUUID(),
|
|
2986
|
+
agentId: id,
|
|
2987
|
+
cwd: runtime.root,
|
|
2988
|
+
newWorktree: false,
|
|
2989
|
+
agent,
|
|
2990
|
+
status: "opening",
|
|
2991
|
+
mcpServers: [],
|
|
2992
|
+
modelId: agent.defaultModelId ?? agent.modelIds[0]
|
|
2993
|
+
};
|
|
2994
|
+
const connection2 = await connectACP(
|
|
2995
|
+
await launchAgent(agent, runtime, info),
|
|
2996
|
+
options,
|
|
2997
|
+
limits
|
|
2998
|
+
);
|
|
2999
|
+
try {
|
|
3000
|
+
return connection2.initialized;
|
|
3001
|
+
} finally {
|
|
3002
|
+
await connection2.close();
|
|
3003
|
+
}
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
3006
|
+
const connection = await connectACP(
|
|
3007
|
+
{
|
|
3008
|
+
type: agent.transport,
|
|
3009
|
+
url: agent.url,
|
|
3010
|
+
headers: resolveEnvironment(agent.headerEnv)
|
|
3011
|
+
},
|
|
3012
|
+
options,
|
|
3013
|
+
limits
|
|
3014
|
+
);
|
|
3015
|
+
try {
|
|
3016
|
+
return connection.initialized;
|
|
3017
|
+
} finally {
|
|
3018
|
+
await connection.close();
|
|
3019
|
+
}
|
|
3020
|
+
})
|
|
3021
|
+
},
|
|
3022
|
+
workspaces: {
|
|
3023
|
+
/**
|
|
3024
|
+
* 新增数据库工作区并归一化存储位置;拒绝目录重叠或同一 volume 重复登记。
|
|
3025
|
+
* @param input - 工作区存储位置、显式 Git 主分支和环境变量引用。
|
|
3026
|
+
* @returns 带来源和版本的工作区条目;重叠位置返回冲突错误。
|
|
3027
|
+
* @example
|
|
3028
|
+
* ```ts
|
|
3029
|
+
* import { ai } from '@h-ai/ai'
|
|
3030
|
+
* // 前提:ai 已成功初始化。
|
|
3031
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3032
|
+
* const result = await ai.acp.workspaces.register({ id: 'project', storage: { type: 'directory', path: '/srv/project' } })
|
|
3033
|
+
* ```
|
|
3034
|
+
*/
|
|
3035
|
+
register: (input) => protectResult(async () => {
|
|
3036
|
+
const workspace = ACPWorkspaceSchema.parse(input);
|
|
3037
|
+
if ((await workspaceRegistry.read(workspace.id))?.source === "config")
|
|
3038
|
+
return workspaceRegistry.operations.register(workspace);
|
|
3039
|
+
await normalizeWorkspace(workspace);
|
|
3040
|
+
const release = await acquire("definition-registry");
|
|
3041
|
+
try {
|
|
3042
|
+
await checkWorkspaceOverlap(workspace);
|
|
3043
|
+
return await workspaceRegistry.operations.register(workspace);
|
|
3044
|
+
} finally {
|
|
3045
|
+
await release();
|
|
3046
|
+
}
|
|
3047
|
+
}),
|
|
3048
|
+
/**
|
|
3049
|
+
* 读取工作区定义及来源;配置条目无数据库版本。
|
|
3050
|
+
* @param id - 工作区 ID。
|
|
3051
|
+
* @returns 工作区资源条目;未找到时返回资源不存在错误。
|
|
3052
|
+
* @example
|
|
3053
|
+
* ```ts
|
|
3054
|
+
* import { ai } from '@h-ai/ai'
|
|
3055
|
+
* // 前提:ai 已成功初始化。
|
|
3056
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3057
|
+
* const result = await ai.acp.workspaces.get('project')
|
|
3058
|
+
* ```
|
|
3059
|
+
*/
|
|
3060
|
+
get: workspaceRegistry.operations.get,
|
|
3061
|
+
/**
|
|
3062
|
+
* 列出配置与数据库合并后的工作区;不创建路径或克隆工程。
|
|
3063
|
+
* @returns 去重后的工作区列表;列表项不含数据库 version。
|
|
3064
|
+
* @example
|
|
3065
|
+
* ```ts
|
|
3066
|
+
* import { ai } from '@h-ai/ai'
|
|
3067
|
+
* // 前提:ai 已成功初始化。
|
|
3068
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3069
|
+
* const result = await ai.acp.workspaces.list()
|
|
3070
|
+
* ```
|
|
3071
|
+
*/
|
|
3072
|
+
list: workspaceRegistry.operations.list,
|
|
3073
|
+
/**
|
|
3074
|
+
* 整体替换无会话引用的数据库工作区,重新检查存储位置重叠;配置条目不可修改。
|
|
3075
|
+
* @param input - 包含原 ID 的完整替换定义。
|
|
3076
|
+
* @param version - 可选的数据库期望版本。
|
|
3077
|
+
* @returns 更新后的工作区;有会话引用、位置冲突或版本冲突时失败。
|
|
3078
|
+
* @example
|
|
3079
|
+
* ```ts
|
|
3080
|
+
* import { ai } from '@h-ai/ai'
|
|
3081
|
+
* // 前提:ai 已成功初始化。
|
|
3082
|
+
* // definition 为完整替换定义;entry 为 get 成功返回的条目。
|
|
3083
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3084
|
+
* const result = await ai.acp.workspaces.update(definition, entry.version)
|
|
3085
|
+
* ```
|
|
3086
|
+
*/
|
|
3087
|
+
update: (input, version) => protectResult(async () => {
|
|
3088
|
+
const workspace = ACPWorkspaceSchema.parse(input);
|
|
3089
|
+
const existing = await workspaceRegistry.read(workspace.id);
|
|
3090
|
+
if (existing?.source === "config")
|
|
3091
|
+
return workspaceRegistry.operations.update(workspace, version);
|
|
3092
|
+
const release = await acquire("definition-registry");
|
|
3093
|
+
try {
|
|
3094
|
+
if ((await sessions.query({ where: { workspaceId: workspace.id } })).length) {
|
|
3095
|
+
throw new ACPFailure("busy");
|
|
3096
|
+
}
|
|
3097
|
+
await normalizeWorkspace(workspace);
|
|
3098
|
+
await checkWorkspaceOverlap(workspace);
|
|
3099
|
+
return await workspaceRegistry.operations.update(
|
|
3100
|
+
workspace,
|
|
3101
|
+
version
|
|
3102
|
+
);
|
|
3103
|
+
} finally {
|
|
3104
|
+
await release();
|
|
3105
|
+
}
|
|
3106
|
+
}),
|
|
3107
|
+
/**
|
|
3108
|
+
* 创建目录并克隆缺失工程;已有内容不自动 pull 或覆盖,Git 不存在时返回明确错误。
|
|
3109
|
+
* @param id - 已登记工作区 ID。
|
|
3110
|
+
* @returns 准备后的 main 绝对目录;克隆失败保留现场供检查。
|
|
3111
|
+
* @example
|
|
3112
|
+
* ```ts
|
|
3113
|
+
* import { ai } from '@h-ai/ai'
|
|
3114
|
+
* // 前提:ai 已成功初始化。
|
|
3115
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3116
|
+
* const result = await ai.acp.workspaces.prepare('project')
|
|
3117
|
+
* ```
|
|
3118
|
+
*/
|
|
3119
|
+
prepare: (id) => protect(
|
|
3120
|
+
async () => workspaceOperation(await getWorkspace(id), (runtime) => createGitOperationsInput(id, runtime))
|
|
3121
|
+
),
|
|
3122
|
+
/**
|
|
3123
|
+
* 将有限大小的文件写入 main/files,拒绝覆盖、路径逃逸和符号链接;失败前已写入文件会保留。
|
|
3124
|
+
* @param id - 已登记工作区 ID。
|
|
3125
|
+
* @param files - 相对路径和 UTF-8 文本或二进制;不允许覆盖或路径逃逸。
|
|
3126
|
+
* @returns 已导入相对路径列表;写入中途失败不回滚已有文件。
|
|
3127
|
+
* @example
|
|
3128
|
+
* ```ts
|
|
3129
|
+
* import { ai } from '@h-ai/ai'
|
|
3130
|
+
* // 前提:ai 已成功初始化。
|
|
3131
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3132
|
+
* const result = await ai.acp.workspaces.importFiles('project', [{ path: 'notes.txt', content: '需求' }])
|
|
3133
|
+
* ```
|
|
3134
|
+
*/
|
|
3135
|
+
importFiles: (id, files) => protect(async () => {
|
|
3136
|
+
if (files.length > 1e3 || files.reduce(
|
|
3137
|
+
(sum, file) => sum + Buffer.byteLength(file.content),
|
|
3138
|
+
0
|
|
3139
|
+
) > limits.maxContentBytes) {
|
|
3140
|
+
throw new ACPFailure("output");
|
|
3141
|
+
}
|
|
3142
|
+
for (const file of files) {
|
|
3143
|
+
if (!file.path || file.path.split(/[\\/]/).some(
|
|
3144
|
+
(part) => !part || part === "." || part === ".." || part === ".git"
|
|
3145
|
+
) || isAbsolute(file.path)) {
|
|
3146
|
+
throw new ACPFailure("invalid");
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
return workspaceOperation(await getWorkspace(id), async (runtime) => {
|
|
3150
|
+
await runtime.file("mkdir", "main/files");
|
|
3151
|
+
for (const file of files) {
|
|
3152
|
+
if (await runtime.file("exists", `main/files/${file.path}`) === "true") {
|
|
3153
|
+
throw new ACPFailure("conflict");
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
for (const file of files) {
|
|
3157
|
+
await runtime.file(
|
|
3158
|
+
"write",
|
|
3159
|
+
`main/files/${file.path}`,
|
|
3160
|
+
Buffer.from(file.content).toString("base64")
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
3163
|
+
return { imported: files.map((file) => file.path) };
|
|
3164
|
+
});
|
|
3165
|
+
}),
|
|
3166
|
+
/**
|
|
3167
|
+
* 移除无会话引用的数据库定义,保留磁盘目录、Git 检出和 volume。
|
|
3168
|
+
* @param id - 数据库工作区 ID。
|
|
3169
|
+
* @param version - 可选的数据库期望版本。
|
|
3170
|
+
* @returns 删除定义的 HaiResult;引用存在时失败,文件始终保留。
|
|
3171
|
+
* @example
|
|
3172
|
+
* ```ts
|
|
3173
|
+
* import { ai } from '@h-ai/ai'
|
|
3174
|
+
* // 前提:ai 已成功初始化。
|
|
3175
|
+
* // entry 为 get 成功返回的数据库条目。
|
|
3176
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3177
|
+
* const result = await ai.acp.workspaces.remove('project', entry.version)
|
|
3178
|
+
* ```
|
|
3179
|
+
*/
|
|
3180
|
+
remove: (id, version) => protectResult(async () => {
|
|
3181
|
+
return withDefinitions(async () => {
|
|
3182
|
+
const existing = await workspaceRegistry.read(id);
|
|
3183
|
+
if (existing?.source === "config")
|
|
3184
|
+
return workspaceRegistry.operations.remove(id, version);
|
|
3185
|
+
if ((await sessions.query({ where: { workspaceId: id } })).length)
|
|
3186
|
+
throw new ACPFailure("busy");
|
|
3187
|
+
return workspaceRegistry.operations.remove(id, version);
|
|
3188
|
+
});
|
|
3189
|
+
})
|
|
3190
|
+
},
|
|
3191
|
+
sessions: {
|
|
3192
|
+
/**
|
|
3193
|
+
* 绑定模型、工作区和 MCP,准备目录后建立原生会话;newWorktree 可选择独立工程分支。
|
|
3194
|
+
* @param options - 智能体、工作区、模型、MCP 及可信回调;newWorktree 默认 false。
|
|
3195
|
+
* @returns 可用于 prompt/stream/publish 的会话句柄;启动失败保留失败记录。
|
|
3196
|
+
* @example
|
|
3197
|
+
* ```ts
|
|
3198
|
+
* import { ai } from '@h-ai/ai'
|
|
3199
|
+
* // 前提:ai 已成功初始化。
|
|
3200
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3201
|
+
* const result = await ai.acp.sessions.create({ agentId: 'coder', workspaceId: 'project', newWorktree: true })
|
|
3202
|
+
* ```
|
|
3203
|
+
*/
|
|
3204
|
+
create: (options) => protect(async () => {
|
|
3205
|
+
const info = await withDefinitions(async () => {
|
|
3206
|
+
const agent = await getAgent(options.agentId);
|
|
3207
|
+
await validateAgent(agent);
|
|
3208
|
+
if (!agent.enabled)
|
|
3209
|
+
throw new ACPFailure("unsupported");
|
|
3210
|
+
const id = sessionIdentitySchema.parse(options.id ?? randomUUID());
|
|
3211
|
+
const modelId = options.modelId ?? agent.defaultModelId ?? agent.modelIds[0];
|
|
3212
|
+
if (modelId && !agent.modelIds.includes(modelId))
|
|
3213
|
+
throw new ACPFailure("model");
|
|
3214
|
+
const info2 = {
|
|
3215
|
+
id,
|
|
3216
|
+
agentId: agent.id,
|
|
3217
|
+
workspaceId: options.workspaceId,
|
|
3218
|
+
cwd: options.cwd ?? "",
|
|
3219
|
+
newWorktree: options.newWorktree ?? false,
|
|
3220
|
+
branch: options.newWorktree ? branchNameSchema.parse(options.branch ?? `acp/${id}`) : void 0,
|
|
3221
|
+
modelId,
|
|
3222
|
+
modelSignature: await modelSignature(agent),
|
|
3223
|
+
status: "opening",
|
|
3224
|
+
agent,
|
|
3225
|
+
mcpServers: options.mcpServers ?? [],
|
|
3226
|
+
mcpServerIds: options.mcpServerIds
|
|
3227
|
+
};
|
|
3228
|
+
if (options.workspaceId)
|
|
3229
|
+
await getWorkspace(options.workspaceId);
|
|
3230
|
+
await unique(sessions, id, info2);
|
|
3231
|
+
return info2;
|
|
3232
|
+
});
|
|
3233
|
+
return start(info, options, false);
|
|
3234
|
+
}),
|
|
3235
|
+
/**
|
|
3236
|
+
* 获取持久会话的管理句柄;已关闭会话需要 resume 后才能执行 prompt。
|
|
3237
|
+
* @param id - 框架会话 ID,不是智能体原生 ID。
|
|
3238
|
+
* @returns 管理句柄的 HaiResult;不会自动重新启动智能体。
|
|
3239
|
+
* @example
|
|
3240
|
+
* ```ts
|
|
3241
|
+
* import { ai } from '@h-ai/ai'
|
|
3242
|
+
* // 前提:ai 已成功初始化。
|
|
3243
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3244
|
+
* const result = await ai.acp.sessions.get('task-1')
|
|
3245
|
+
* ```
|
|
3246
|
+
*/
|
|
3247
|
+
get: (id) => protect(async () => {
|
|
3248
|
+
await get(sessions, id);
|
|
3249
|
+
return handle(id);
|
|
3250
|
+
}),
|
|
3251
|
+
/**
|
|
3252
|
+
* 列出持久会话,可按工作区 ID 过滤。
|
|
3253
|
+
* @param workspaceId - 可选工作区筛选条件;省略返回全部会话。
|
|
3254
|
+
* @returns 持久 ACPSessionInfo 列表的 HaiResult。
|
|
3255
|
+
* @example
|
|
3256
|
+
* ```ts
|
|
3257
|
+
* import { ai } from '@h-ai/ai'
|
|
3258
|
+
* // 前提:ai 已成功初始化。
|
|
3259
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3260
|
+
* const result = await ai.acp.sessions.list('project')
|
|
3261
|
+
* ```
|
|
3262
|
+
*/
|
|
3263
|
+
list: (workspaceId) => protect(
|
|
3264
|
+
() => sessions.query({ where: workspaceId ? { workspaceId } : {} })
|
|
3265
|
+
),
|
|
3266
|
+
/**
|
|
3267
|
+
* 使用原生会话 ID 和保留状态恢复;智能体定义或模型能力已改变时拒绝恢复。
|
|
3268
|
+
* @param id - 已关闭或失败的框架会话 ID。
|
|
3269
|
+
* @param options - 本次连接的回调、能力和取消信号,不从数据库恢复回调。
|
|
3270
|
+
* @returns 恢复后的会话句柄;原生服务不支持恢复或配置变化时失败。
|
|
3271
|
+
* @example
|
|
3272
|
+
* ```ts
|
|
3273
|
+
* import { ai } from '@h-ai/ai'
|
|
3274
|
+
* // 前提:ai 已成功初始化。
|
|
3275
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3276
|
+
* const result = await ai.acp.sessions.resume('task-1')
|
|
3277
|
+
* ```
|
|
3278
|
+
*/
|
|
3279
|
+
resume: (id, options = {}) => protect(async () => {
|
|
3280
|
+
const info = await get(sessions, id);
|
|
3281
|
+
if (info.status === "opening" || info.status === "active")
|
|
3282
|
+
throw new ACPFailure("busy");
|
|
3283
|
+
const agent = await getAgent(info.agentId);
|
|
3284
|
+
if (!agent.enabled || JSON.stringify(agent) !== JSON.stringify(info.agent)) {
|
|
3285
|
+
throw new ACPFailure("conflict");
|
|
3286
|
+
}
|
|
3287
|
+
if (info.modelSignature !== await modelSignature(agent))
|
|
3288
|
+
throw new ACPFailure("conflict");
|
|
3289
|
+
return start(info, options, true);
|
|
3290
|
+
}),
|
|
3291
|
+
/**
|
|
3292
|
+
* 关闭本进程拥有的会话并释放锁,不删除历史和工程内容。
|
|
3293
|
+
* @param id - 框架会话 ID。
|
|
3294
|
+
* @returns 关闭完成的 HaiResult;不允许关闭其他进程拥有的执行环境。
|
|
3295
|
+
* @example
|
|
3296
|
+
* ```ts
|
|
3297
|
+
* import { ai } from '@h-ai/ai'
|
|
3298
|
+
* // 前提:ai 已成功初始化。
|
|
3299
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3300
|
+
* const result = await ai.acp.sessions.close('task-1')
|
|
3301
|
+
* ```
|
|
3302
|
+
*/
|
|
3303
|
+
close: (id) => protect(() => stop(id)),
|
|
3304
|
+
/**
|
|
3305
|
+
* 仅删除已关闭或失败的会话记录;不删除工作树及原生文件,不替代 Git worktree 清理。
|
|
3306
|
+
* @param id - 框架会话 ID。
|
|
3307
|
+
* @returns 删除完成的 HaiResult;活动会话返回忙碌错误。
|
|
3308
|
+
* @example
|
|
3309
|
+
* ```ts
|
|
3310
|
+
* import { ai } from '@h-ai/ai'
|
|
3311
|
+
* // 前提:ai 已成功初始化。
|
|
3312
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3313
|
+
* const result = await ai.acp.sessions.remove('task-1')
|
|
3314
|
+
* ```
|
|
3315
|
+
*/
|
|
3316
|
+
remove: (id) => protect(async () => {
|
|
3317
|
+
const info = await get(sessions, id);
|
|
3318
|
+
if (live.has(id) || info.status === "active" || info.status === "opening") {
|
|
3319
|
+
throw new ACPFailure("busy");
|
|
3320
|
+
}
|
|
3321
|
+
const release = await acquire(`session:${id}`);
|
|
3322
|
+
try {
|
|
3323
|
+
await sessions.remove(id);
|
|
3324
|
+
} finally {
|
|
3325
|
+
await release();
|
|
3326
|
+
}
|
|
3327
|
+
})
|
|
3328
|
+
},
|
|
3329
|
+
/**
|
|
3330
|
+
* 连接原生 stdio/HTTP/WS 服务;不创建工作区或提供传输兼容桥。
|
|
3331
|
+
* @param transport - 智能体实际支持的原生传输与命令或地址。
|
|
3332
|
+
* @param options - 可信回调、声明能力和初始化取消信号。
|
|
3333
|
+
* @returns 低层连接句柄的 HaiResult;连接失败不自动转换传输。
|
|
3334
|
+
* @example
|
|
3335
|
+
* ```ts
|
|
3336
|
+
* import { ai } from '@h-ai/ai'
|
|
3337
|
+
* // 前提:ai 已成功初始化。
|
|
3338
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3339
|
+
* const result = await ai.acp.connect({ type: 'http', url: 'https://agent.example.com/acp' })
|
|
3340
|
+
* ```
|
|
3341
|
+
*/
|
|
3342
|
+
connect: (transport, options = {}) => protect(async () => {
|
|
3343
|
+
const connection = await connectACP(transport, options, limits);
|
|
3344
|
+
connections.add(connection);
|
|
3345
|
+
return {
|
|
3346
|
+
info: connection.initialized,
|
|
3347
|
+
/**
|
|
3348
|
+
* 选择对端握手公布的认证方法,返回 HaiResult。
|
|
3349
|
+
* @param method - initialize 返回的认证方法 ID。
|
|
3350
|
+
* @returns 认证响应的 HaiResult。
|
|
3351
|
+
* @example
|
|
3352
|
+
* ```ts
|
|
3353
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3354
|
+
* const result = await connection.authenticate('api-key')
|
|
3355
|
+
* ```
|
|
3356
|
+
*/
|
|
3357
|
+
authenticate: (method) => protect(() => connection.authenticate(method)),
|
|
3358
|
+
/**
|
|
3359
|
+
* 在对端可见的绝对 cwd 创建原生会话,返回 prompt/cancel 句柄。
|
|
3360
|
+
* @param cwd - 对端文件系统可见的绝对工作目录。
|
|
3361
|
+
* @param mcpServers - 可选原生 MCP 定义;调用方负责凭据与授权。
|
|
3362
|
+
* @returns 原生会话句柄;不创建框架持久会话记录。
|
|
3363
|
+
* @example
|
|
3364
|
+
* ```ts
|
|
3365
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3366
|
+
* const result = await connection.createSession('/srv/project', [])
|
|
3367
|
+
* ```
|
|
3368
|
+
*/
|
|
3369
|
+
createSession: (cwd, mcpServers) => protect(async () => {
|
|
3370
|
+
const session = await connection.createSession(cwd, mcpServers);
|
|
3371
|
+
return {
|
|
3372
|
+
id: session.sessionId,
|
|
3373
|
+
/**
|
|
3374
|
+
* 发送一轮原生内容,返回最终文本、停止原因和事件。
|
|
3375
|
+
* @param content - 原生 ACP 内容块。
|
|
3376
|
+
* @param signal - 可选本轮取消信号。
|
|
3377
|
+
* @returns 最终文本、停止原因和事件的 HaiResult。
|
|
3378
|
+
* @example
|
|
3379
|
+
* ```ts
|
|
3380
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3381
|
+
* const result = await session.prompt([{ type: 'text', text: '检查工程' }])
|
|
3382
|
+
* ```
|
|
3383
|
+
*/
|
|
3384
|
+
prompt: (content, signal) => protect(() => connection.prompt(content, signal)),
|
|
3385
|
+
/**
|
|
3386
|
+
* 取消当前轮次,超时则关闭连接。
|
|
3387
|
+
* @returns 取消完成的 HaiResult。
|
|
3388
|
+
* @example
|
|
3389
|
+
* ```ts
|
|
3390
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3391
|
+
* const result = await session.cancel()
|
|
3392
|
+
* ```
|
|
3393
|
+
*/
|
|
3394
|
+
cancel: () => protect(() => connection.cancel())
|
|
3395
|
+
};
|
|
3396
|
+
}),
|
|
3397
|
+
/**
|
|
3398
|
+
* 读取对端原生会话列表;未声明相应能力时返回错误。
|
|
3399
|
+
* @param cursor - 上一次原生列表响应中的分页游标。
|
|
3400
|
+
* @returns 原生分页响应的 HaiResult。
|
|
3401
|
+
* @example
|
|
3402
|
+
* ```ts
|
|
3403
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3404
|
+
* const result = await connection.listSessions()
|
|
3405
|
+
* ```
|
|
3406
|
+
*/
|
|
3407
|
+
listSessions: (cursor) => protect(() => connection.listSessions(cursor)),
|
|
3408
|
+
/**
|
|
3409
|
+
* 按原生 ID 删除远端会话;不会删除框架数据库会话记录。
|
|
3410
|
+
* @param id - 对端返回的原生会话 ID。
|
|
3411
|
+
* @returns 原生删除响应的 HaiResult;不支持时返回协议错误。
|
|
3412
|
+
* @example
|
|
3413
|
+
* ```ts
|
|
3414
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3415
|
+
* const result = await connection.deleteSession('native-id')
|
|
3416
|
+
* ```
|
|
3417
|
+
*/
|
|
3418
|
+
deleteSession: (id) => protect(() => connection.deleteSession(id)),
|
|
3419
|
+
/**
|
|
3420
|
+
* 关闭连接和拥有的子进程;不删除远端文件或持久状态。
|
|
3421
|
+
* @returns 关闭完成的 HaiResult;可重复关闭。
|
|
3422
|
+
* @example
|
|
3423
|
+
* ```ts
|
|
3424
|
+
* // connection/session 为已成功连接或创建的句柄,先检查 result.success。
|
|
3425
|
+
* const result = await connection.close()
|
|
3426
|
+
* ```
|
|
3427
|
+
*/
|
|
3428
|
+
close: () => protect(async () => {
|
|
3429
|
+
await connection.close();
|
|
3430
|
+
connections.delete(connection);
|
|
3431
|
+
})
|
|
3432
|
+
};
|
|
3433
|
+
}),
|
|
3434
|
+
/**
|
|
3435
|
+
* 关闭子系统及拥有的资源;由 ai.close 生命周期调用。
|
|
3436
|
+
* @returns 所有已接受操作结束和拥有资源完成清理后的 Promise。
|
|
3437
|
+
* @throws 清理进程或容器失败时抛出 ACPFailure,由 ai.close 统一处理。
|
|
3438
|
+
* @example
|
|
3439
|
+
* ```ts
|
|
3440
|
+
* import { ai } from '@h-ai/ai'
|
|
3441
|
+
* // 前提:ai 已成功初始化。
|
|
3442
|
+
* // 普通 API 先检查 result.success,再读取 data 或 error。
|
|
3443
|
+
* await ai.close()
|
|
3444
|
+
* ```
|
|
3445
|
+
*/
|
|
3446
|
+
async close() {
|
|
3447
|
+
closed = true;
|
|
3448
|
+
await Promise.allSettled(
|
|
3449
|
+
[...live.values()].map((value) => value.connection.cancel())
|
|
3450
|
+
);
|
|
3451
|
+
await Promise.allSettled(
|
|
3452
|
+
[...connections].map((connection) => connection.close())
|
|
3453
|
+
);
|
|
3454
|
+
await Promise.allSettled([...pending]);
|
|
3455
|
+
const results = await Promise.allSettled([...live.keys()].map(stop));
|
|
3456
|
+
const more = await Promise.allSettled(
|
|
3457
|
+
[...connections].map((connection) => connection.close())
|
|
3458
|
+
);
|
|
3459
|
+
connections.clear();
|
|
3460
|
+
if ([...results, ...more].some((result) => result.status === "rejected"))
|
|
3461
|
+
throw new ACPFailure("cleanup");
|
|
3462
|
+
}
|
|
3463
|
+
};
|
|
3464
|
+
async function createGitOperationsInput(id, runtime) {
|
|
3465
|
+
return createGitOperations(await getWorkspace(id), runtime).prepare();
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
705
3468
|
var SENTENCE_BOUNDARY_REGEX = /[。!?;!?;\n]|\.(?=\s|$)/;
|
|
706
3469
|
async function* streamSentences(stream) {
|
|
707
3470
|
let buffer = "";
|
|
@@ -799,8 +3562,8 @@ async function describeHttpError(response) {
|
|
|
799
3562
|
}
|
|
800
3563
|
var logger3 = core.logger.child({ module: "ai", scope: "audio-ws" });
|
|
801
3564
|
function openAudioWebSocket(url, headers, options) {
|
|
802
|
-
return new Promise((
|
|
803
|
-
const ws = new
|
|
3565
|
+
return new Promise((resolve3, reject) => {
|
|
3566
|
+
const ws = new WebSocket2(url, { headers, handshakeTimeout: options?.timeout ?? 15e3 });
|
|
804
3567
|
ws.binaryType = "arraybuffer";
|
|
805
3568
|
const queue = [];
|
|
806
3569
|
let waiting = null;
|
|
@@ -903,7 +3666,7 @@ function openAudioWebSocket(url, headers, options) {
|
|
|
903
3666
|
}
|
|
904
3667
|
}
|
|
905
3668
|
};
|
|
906
|
-
|
|
3669
|
+
resolve3(connection);
|
|
907
3670
|
});
|
|
908
3671
|
});
|
|
909
3672
|
}
|
|
@@ -914,7 +3677,7 @@ var DOUBAO_CAPABILITIES = {
|
|
|
914
3677
|
transcribe: { supported: true, realtimeAudioInput: true, speechBoundaryEvents: false, streamingTranscriptOutput: true },
|
|
915
3678
|
synthesize: { supported: true, incrementalTextInput: true, streamingAudioOutput: true }
|
|
916
3679
|
};
|
|
917
|
-
var
|
|
3680
|
+
var PROTOCOL_VERSION2 = 1;
|
|
918
3681
|
var HEADER_SIZE = 1;
|
|
919
3682
|
var MSG_FULL_CLIENT = 1;
|
|
920
3683
|
var MSG_AUDIO_ONLY_CLIENT = 2;
|
|
@@ -952,7 +3715,7 @@ var DOUBAO_FORMAT = {
|
|
|
952
3715
|
};
|
|
953
3716
|
function buildHeader(messageType, flags, serialization, compression) {
|
|
954
3717
|
return Buffer.from([
|
|
955
|
-
|
|
3718
|
+
PROTOCOL_VERSION2 << 4 | HEADER_SIZE,
|
|
956
3719
|
messageType << 4 | flags,
|
|
957
3720
|
serialization << 4 | compression,
|
|
958
3721
|
0
|
|
@@ -1726,9 +4489,9 @@ function createOpenAIAudioProvider() {
|
|
|
1726
4489
|
if (!model.apiKey)
|
|
1727
4490
|
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_audioMissingApiKey", { params: { provider: "openai" } }));
|
|
1728
4491
|
try {
|
|
1729
|
-
const
|
|
4492
|
+
const client2 = createClient(model.apiKey, model.baseUrl, model.timeout);
|
|
1730
4493
|
const file = await toUploadFile(audio);
|
|
1731
|
-
const result = await
|
|
4494
|
+
const result = await client2.audio.transcriptions.create({
|
|
1732
4495
|
file,
|
|
1733
4496
|
model: model.model,
|
|
1734
4497
|
language,
|
|
@@ -1750,9 +4513,9 @@ function createOpenAIAudioProvider() {
|
|
|
1750
4513
|
aiM("ai_audioUnsupportedInput", { params: { provider: "openai", reason: "streaming audio input" } })
|
|
1751
4514
|
);
|
|
1752
4515
|
}
|
|
1753
|
-
const
|
|
4516
|
+
const client2 = createClient(model.apiKey, model.baseUrl, model.timeout);
|
|
1754
4517
|
const file = await toUploadFile(audio);
|
|
1755
|
-
const stream = await
|
|
4518
|
+
const stream = await client2.audio.transcriptions.create({
|
|
1756
4519
|
file,
|
|
1757
4520
|
model: model.model,
|
|
1758
4521
|
language,
|
|
@@ -1775,8 +4538,8 @@ function createOpenAIAudioProvider() {
|
|
|
1775
4538
|
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_audioMissingApiKey", { params: { provider: "openai" } }));
|
|
1776
4539
|
const out = resolveSynthesisOutput4({ format, sampleRate });
|
|
1777
4540
|
try {
|
|
1778
|
-
const
|
|
1779
|
-
const response = await
|
|
4541
|
+
const client2 = createClient(model.apiKey, model.baseUrl, model.timeout);
|
|
4542
|
+
const response = await client2.audio.speech.create({
|
|
1780
4543
|
model: model.model,
|
|
1781
4544
|
voice: voice ?? "alloy",
|
|
1782
4545
|
input: text,
|
|
@@ -1795,12 +4558,12 @@ function createOpenAIAudioProvider() {
|
|
|
1795
4558
|
if (!model.apiKey)
|
|
1796
4559
|
throw audioError(HaiAIError.CONFIGURATION_ERROR, aiM("ai_audioMissingApiKey", { params: { provider: "openai" } }));
|
|
1797
4560
|
const outFormat = resolveSynthesisOutput4({ format }).format;
|
|
1798
|
-
const
|
|
4561
|
+
const client2 = createClient(model.apiKey, model.baseUrl, model.timeout);
|
|
1799
4562
|
const segments = typeof text === "string" ? [text] : streamSentences(text);
|
|
1800
4563
|
for await (const segment of segments) {
|
|
1801
4564
|
if (!segment)
|
|
1802
4565
|
continue;
|
|
1803
|
-
const response = await
|
|
4566
|
+
const response = await client2.audio.speech.create({
|
|
1804
4567
|
model: model.model,
|
|
1805
4568
|
voice: voice ?? "alloy",
|
|
1806
4569
|
input: segment,
|
|
@@ -2854,12 +5617,12 @@ function createContextOperations(compressConfig, tokenOps, compressOps, contextS
|
|
|
2854
5617
|
delete candidate.active;
|
|
2855
5618
|
}
|
|
2856
5619
|
async function waitForContext(signal) {
|
|
2857
|
-
await new Promise((
|
|
5620
|
+
await new Promise((resolve3, reject) => {
|
|
2858
5621
|
const timer = setTimeout(finish, 50);
|
|
2859
5622
|
function finish() {
|
|
2860
5623
|
clearTimeout(timer);
|
|
2861
5624
|
signal?.removeEventListener("abort", abort);
|
|
2862
|
-
|
|
5625
|
+
resolve3();
|
|
2863
5626
|
}
|
|
2864
5627
|
function abort() {
|
|
2865
5628
|
clearTimeout(timer);
|
|
@@ -3579,7 +6342,7 @@ function createEmbeddingOperations(config) {
|
|
|
3579
6342
|
const resolved = resolvedResult.data;
|
|
3580
6343
|
const input = Array.isArray(request.input) ? request.input : [request.input];
|
|
3581
6344
|
try {
|
|
3582
|
-
const
|
|
6345
|
+
const client2 = getOrCreateClient(resolved);
|
|
3583
6346
|
const params = {
|
|
3584
6347
|
model: resolved.model,
|
|
3585
6348
|
input
|
|
@@ -3587,7 +6350,7 @@ function createEmbeddingOperations(config) {
|
|
|
3587
6350
|
if (request.dimensions ?? embeddingConfig.dimensions) {
|
|
3588
6351
|
params.dimensions = request.dimensions ?? embeddingConfig.dimensions;
|
|
3589
6352
|
}
|
|
3590
|
-
const response = await
|
|
6353
|
+
const response = await client2.embeddings.create(params);
|
|
3591
6354
|
return ok({
|
|
3592
6355
|
model: response.model,
|
|
3593
6356
|
data: response.data.map((item) => ({
|
|
@@ -5217,9 +7980,9 @@ ${contextText}
|
|
|
5217
7980
|
async ingestFile(input) {
|
|
5218
7981
|
try {
|
|
5219
7982
|
const { readFile } = await import('fs/promises');
|
|
5220
|
-
const { basename } = await import('path');
|
|
7983
|
+
const { basename: basename2 } = await import('path');
|
|
5221
7984
|
const content = await readFile(input.filePath, { encoding: input.encoding ?? "utf-8" });
|
|
5222
|
-
const fileName =
|
|
7985
|
+
const fileName = basename2(input.filePath);
|
|
5223
7986
|
const documentId = input.documentId ?? fileName;
|
|
5224
7987
|
return this.ingest({
|
|
5225
7988
|
documentId,
|
|
@@ -5294,13 +8057,13 @@ async function runWithCancellation(run, context) {
|
|
|
5294
8057
|
const { signal } = context;
|
|
5295
8058
|
if (signal.aborted)
|
|
5296
8059
|
throw new DOMException("Aborted", "AbortError");
|
|
5297
|
-
return await new Promise((
|
|
8060
|
+
return await new Promise((resolve3, reject) => {
|
|
5298
8061
|
const onAbort = () => reject(new DOMException("Aborted", "AbortError"));
|
|
5299
8062
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
5300
8063
|
Promise.resolve().then(() => run(context)).then(
|
|
5301
8064
|
(value) => {
|
|
5302
8065
|
signal.removeEventListener("abort", onAbort);
|
|
5303
|
-
|
|
8066
|
+
resolve3(value);
|
|
5304
8067
|
},
|
|
5305
8068
|
(error) => {
|
|
5306
8069
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -5612,11 +8375,11 @@ function createAnthropicProvider(deps) {
|
|
|
5612
8375
|
function resolveAnthropic(requestModel, tempModel) {
|
|
5613
8376
|
const entry = requestModel ? config.llm.models?.find((m) => m.id === requestModel || m.model === requestModel) : void 0;
|
|
5614
8377
|
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 ?? (
|
|
8378
|
+
const apiKey = tempModel?.apiKey ?? entry?.apiKey ?? config.llm.apiKey ?? (process2.env.HAI_AI_LLM_APIKEY?.trim() || process2.env.ANTHROPIC_API_KEY);
|
|
5616
8379
|
if (!apiKey)
|
|
5617
8380
|
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_configError", { params: { error: "API Key is required for Anthropic" } }));
|
|
5618
|
-
const globalBaseUrl = config.llm.protocol === "anthropic" ? config.llm.baseUrl ?? (
|
|
5619
|
-
const baseUrl = (tempModel?.baseUrl ?? entry?.baseUrl ?? globalBaseUrl ??
|
|
8381
|
+
const globalBaseUrl = config.llm.protocol === "anthropic" ? config.llm.baseUrl ?? (process2.env.HAI_AI_LLM_BASEURL?.trim() || void 0) : void 0;
|
|
8382
|
+
const baseUrl = (tempModel?.baseUrl ?? entry?.baseUrl ?? globalBaseUrl ?? process2.env.ANTHROPIC_BASE_URL ?? DEFAULT_ANTHROPIC_BASE_URL).replace(/\/+$/, "");
|
|
5620
8383
|
return ok({
|
|
5621
8384
|
apiKey,
|
|
5622
8385
|
baseUrl,
|
|
@@ -5926,12 +8689,12 @@ function createOpenAIProvider(deps) {
|
|
|
5926
8689
|
const tempCacheTtl = config.llm.tempModelCacheTtl ?? 6e5;
|
|
5927
8690
|
function getClient(apiKey, baseURL, timeout) {
|
|
5928
8691
|
const key = `${apiKey}::${baseURL}::${timeout}`;
|
|
5929
|
-
let
|
|
5930
|
-
if (!
|
|
5931
|
-
|
|
5932
|
-
clientCache.set(key,
|
|
8692
|
+
let client2 = clientCache.get(key);
|
|
8693
|
+
if (!client2) {
|
|
8694
|
+
client2 = new OpenAI3({ apiKey, baseURL, timeout });
|
|
8695
|
+
clientCache.set(key, client2);
|
|
5933
8696
|
}
|
|
5934
|
-
return
|
|
8697
|
+
return client2;
|
|
5935
8698
|
}
|
|
5936
8699
|
function getTempClient(apiKey, baseURL, timeout) {
|
|
5937
8700
|
const now = Date.now();
|
|
@@ -5943,9 +8706,9 @@ function createOpenAIProvider(deps) {
|
|
|
5943
8706
|
const cached = tempClientCache.get(key);
|
|
5944
8707
|
if (cached)
|
|
5945
8708
|
return cached.client;
|
|
5946
|
-
const
|
|
5947
|
-
tempClientCache.set(key, { client, expiresAt: now + tempCacheTtl });
|
|
5948
|
-
return
|
|
8709
|
+
const client2 = new OpenAI3({ apiKey, baseURL, timeout });
|
|
8710
|
+
tempClientCache.set(key, { client: client2, expiresAt: now + tempCacheTtl });
|
|
8711
|
+
return client2;
|
|
5949
8712
|
}
|
|
5950
8713
|
function resolveClient(requestModel, tempModel) {
|
|
5951
8714
|
if (tempModel)
|
|
@@ -5960,10 +8723,10 @@ function createOpenAIProvider(deps) {
|
|
|
5960
8723
|
return ok({ client: getClient(apiKey ?? "", baseUrl ?? "https://api.openai.com/v1", timeout), model, protocol: resolvedApi });
|
|
5961
8724
|
}
|
|
5962
8725
|
function resolveTempClient(tempModel) {
|
|
5963
|
-
const apiKey = tempModel.apiKey ?? config.llm.apiKey ?? (
|
|
8726
|
+
const apiKey = tempModel.apiKey ?? config.llm.apiKey ?? (process2.env.HAI_AI_LLM_APIKEY?.trim() || process2.env.OPENAI_API_KEY);
|
|
5964
8727
|
if (!apiKey)
|
|
5965
8728
|
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 ?? (
|
|
8729
|
+
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
8730
|
const timeout = tempModel.timeout ?? config.llm.timeout ?? 6e4;
|
|
5968
8731
|
const api = (tempModel.protocol ?? config.llm.protocol) === "responses" ? "responses" : "chat";
|
|
5969
8732
|
return ok({
|
|
@@ -5995,11 +8758,11 @@ function createOpenAIProvider(deps) {
|
|
|
5995
8758
|
if (!clientResult.success)
|
|
5996
8759
|
return clientResult;
|
|
5997
8760
|
const resolved = clientResult.data;
|
|
5998
|
-
const { client, model, maxTokens, temperature, protocol: api } = resolved;
|
|
8761
|
+
const { client: client2, model, maxTokens, temperature, protocol: api } = resolved;
|
|
5999
8762
|
const { objectId: _objectId, sessionId: _sessionId, tempModel: _tempModel, signal, ...openaiRequest } = request;
|
|
6000
8763
|
try {
|
|
6001
8764
|
if (api === "responses") {
|
|
6002
|
-
const response2 = await
|
|
8765
|
+
const response2 = await client2.responses.create(buildResponsesParams(request, resolved), { signal });
|
|
6003
8766
|
return ok(responsesToChatResponse(response2, model));
|
|
6004
8767
|
}
|
|
6005
8768
|
const openaiMessages = request.messages.map(toOpenAIMessage);
|
|
@@ -6011,7 +8774,7 @@ function createOpenAIProvider(deps) {
|
|
|
6011
8774
|
...temperature !== void 0 && openaiRequest.temperature == null ? { temperature } : {},
|
|
6012
8775
|
stream: false
|
|
6013
8776
|
};
|
|
6014
|
-
const response = signal ? await
|
|
8777
|
+
const response = signal ? await client2.chat.completions.create(params, { signal }) : await client2.chat.completions.create(params);
|
|
6015
8778
|
return ok(response);
|
|
6016
8779
|
} catch (error) {
|
|
6017
8780
|
const mapped = toAIError(error);
|
|
@@ -6023,10 +8786,10 @@ function createOpenAIProvider(deps) {
|
|
|
6023
8786
|
if (!clientResult.success)
|
|
6024
8787
|
throw new Error(clientResult.error.message);
|
|
6025
8788
|
const resolved = clientResult.data;
|
|
6026
|
-
const { client, model, maxTokens, temperature, protocol: api } = resolved;
|
|
8789
|
+
const { client: client2, model, maxTokens, temperature, protocol: api } = resolved;
|
|
6027
8790
|
const { objectId: _objectId, sessionId: _sessionId, tempModel: _tempModel, signal, ...openaiRequest } = request;
|
|
6028
8791
|
if (api === "responses") {
|
|
6029
|
-
const responsesStream = await
|
|
8792
|
+
const responsesStream = await client2.responses.create(
|
|
6030
8793
|
{ ...buildResponsesParams(request, resolved), stream: true },
|
|
6031
8794
|
{ signal }
|
|
6032
8795
|
);
|
|
@@ -6046,7 +8809,7 @@ function createOpenAIProvider(deps) {
|
|
|
6046
8809
|
...temperature !== void 0 && openaiRequest.temperature == null ? { temperature } : {},
|
|
6047
8810
|
stream: true
|
|
6048
8811
|
};
|
|
6049
|
-
const stream = signal ? await
|
|
8812
|
+
const stream = signal ? await client2.chat.completions.create(params, { signal }) : await client2.chat.completions.create(params);
|
|
6050
8813
|
for await (const chunk of stream) {
|
|
6051
8814
|
yield chunk;
|
|
6052
8815
|
}
|
|
@@ -6055,9 +8818,9 @@ function createOpenAIProvider(deps) {
|
|
|
6055
8818
|
const clientResult = resolveClient();
|
|
6056
8819
|
if (!clientResult.success)
|
|
6057
8820
|
return clientResult;
|
|
6058
|
-
const { client } = clientResult.data;
|
|
8821
|
+
const { client: client2 } = clientResult.data;
|
|
6059
8822
|
try {
|
|
6060
|
-
const response = await
|
|
8823
|
+
const response = await client2.models.list();
|
|
6061
8824
|
const models = response.data.map((m) => m.id);
|
|
6062
8825
|
return ok(models);
|
|
6063
8826
|
} catch (error) {
|
|
@@ -6093,7 +8856,19 @@ function createLLMProvider(deps) {
|
|
|
6093
8856
|
// src/llm/ai-llm-functions.ts
|
|
6094
8857
|
var logger18 = core.logger.child({ module: "ai", scope: "llm" });
|
|
6095
8858
|
function createAILLMFunctions(config, deps) {
|
|
6096
|
-
|
|
8859
|
+
let provider = createLLMProvider({ config });
|
|
8860
|
+
let signature = JSON.stringify(config.llm);
|
|
8861
|
+
async function currentProvider() {
|
|
8862
|
+
if (!deps?.resolveConfig)
|
|
8863
|
+
return provider;
|
|
8864
|
+
const snapshot2 = await deps.resolveConfig();
|
|
8865
|
+
const next = JSON.stringify(snapshot2.llm);
|
|
8866
|
+
if (next !== signature) {
|
|
8867
|
+
provider = createLLMProvider({ config: snapshot2 });
|
|
8868
|
+
signature = next;
|
|
8869
|
+
}
|
|
8870
|
+
return provider;
|
|
8871
|
+
}
|
|
6097
8872
|
const recordStore = deps?.recordStore;
|
|
6098
8873
|
const sessionStore = deps?.sessionStore;
|
|
6099
8874
|
let recordSeq = 0;
|
|
@@ -6108,13 +8883,26 @@ function createAILLMFunctions(config, deps) {
|
|
|
6108
8883
|
return "New Session";
|
|
6109
8884
|
}
|
|
6110
8885
|
async function saveRecordAndSession(record, messages) {
|
|
6111
|
-
await recordStore.save(record.id, record, {
|
|
6112
|
-
|
|
8886
|
+
await recordStore.save(record.id, record, {
|
|
8887
|
+
objectId: record.objectId,
|
|
8888
|
+
sessionId: record.sessionId
|
|
8889
|
+
});
|
|
8890
|
+
logger18.trace("Chat record saved", {
|
|
8891
|
+
id: record.id,
|
|
8892
|
+
objectId: record.objectId,
|
|
8893
|
+
sessionId: record.sessionId
|
|
8894
|
+
});
|
|
6113
8895
|
if (sessionStore) {
|
|
6114
8896
|
await updateSessionInfo(sessionStore, record, (existing, now) => {
|
|
6115
8897
|
if (existing?.deleted)
|
|
6116
8898
|
return void 0;
|
|
6117
|
-
return existing ? { ...existing, updatedAt: now } : {
|
|
8899
|
+
return existing ? { ...existing, updatedAt: now } : {
|
|
8900
|
+
sessionId: record.sessionId,
|
|
8901
|
+
objectId: record.objectId,
|
|
8902
|
+
title: extractTitle(messages),
|
|
8903
|
+
createdAt: now,
|
|
8904
|
+
updatedAt: now
|
|
8905
|
+
};
|
|
6118
8906
|
});
|
|
6119
8907
|
}
|
|
6120
8908
|
}
|
|
@@ -6122,9 +8910,20 @@ function createAILLMFunctions(config, deps) {
|
|
|
6122
8910
|
if (request.tempModel && Object.hasOwn(request.tempModel, "api"))
|
|
6123
8911
|
return err(HaiAIError.INVALID_REQUEST, aiM("ai_llmProtocolRenamed"));
|
|
6124
8912
|
const start = Date.now();
|
|
6125
|
-
const context = {
|
|
8913
|
+
const context = {
|
|
8914
|
+
model: request.model ?? request.tempModel?.model,
|
|
8915
|
+
messageCount: request.messages.length
|
|
8916
|
+
};
|
|
6126
8917
|
logger18.debug("LLM chat started", context);
|
|
6127
|
-
|
|
8918
|
+
let result;
|
|
8919
|
+
try {
|
|
8920
|
+
result = await (await currentProvider()).chat(request);
|
|
8921
|
+
} catch {
|
|
8922
|
+
return err(
|
|
8923
|
+
HaiAIError.RESOURCE_OPERATION_FAILED,
|
|
8924
|
+
aiM("ai_resourceOperationFailed")
|
|
8925
|
+
);
|
|
8926
|
+
}
|
|
6128
8927
|
if (result.success) {
|
|
6129
8928
|
logger18.info("LLM chat completed", {
|
|
6130
8929
|
...context,
|
|
@@ -6157,7 +8956,11 @@ function createAILLMFunctions(config, deps) {
|
|
|
6157
8956
|
content: choice?.message?.content ?? "",
|
|
6158
8957
|
toolCalls: choice?.message?.tool_calls,
|
|
6159
8958
|
finishReason: choice?.finish_reason ?? "stop",
|
|
6160
|
-
usage: response.usage ?? {
|
|
8959
|
+
usage: response.usage ?? {
|
|
8960
|
+
prompt_tokens: 0,
|
|
8961
|
+
completion_tokens: 0,
|
|
8962
|
+
total_tokens: 0
|
|
8963
|
+
}
|
|
6161
8964
|
},
|
|
6162
8965
|
createdAt: Date.now(),
|
|
6163
8966
|
duration: Date.now() - start
|
|
@@ -6173,7 +8976,10 @@ function createAILLMFunctions(config, deps) {
|
|
|
6173
8976
|
if (request.tempModel && Object.hasOwn(request.tempModel, "api"))
|
|
6174
8977
|
throw new Error(aiM("ai_llmProtocolRenamed"));
|
|
6175
8978
|
const start = Date.now();
|
|
6176
|
-
const logContext = {
|
|
8979
|
+
const logContext = {
|
|
8980
|
+
model: request.model ?? request.tempModel?.model,
|
|
8981
|
+
messageCount: request.messages.length
|
|
8982
|
+
};
|
|
6177
8983
|
logger18.debug("LLM chat stream started", logContext);
|
|
6178
8984
|
const shouldRecord = !!(request.objectId && recordStore && request.enablePersist !== false);
|
|
6179
8985
|
let content = "";
|
|
@@ -6182,7 +8988,7 @@ function createAILLMFunctions(config, deps) {
|
|
|
6182
8988
|
let streamId = "";
|
|
6183
8989
|
let resolvedModel = request.model ?? request.tempModel?.model ?? "";
|
|
6184
8990
|
try {
|
|
6185
|
-
for await (const chunk of
|
|
8991
|
+
for await (const chunk of (await currentProvider()).chatStream(request)) {
|
|
6186
8992
|
yield chunk;
|
|
6187
8993
|
if (!shouldRecord)
|
|
6188
8994
|
continue;
|
|
@@ -6198,7 +9004,11 @@ function createAILLMFunctions(config, deps) {
|
|
|
6198
9004
|
if (choice.delta.tool_calls) {
|
|
6199
9005
|
for (const tc of choice.delta.tool_calls) {
|
|
6200
9006
|
if (!toolCalls[tc.index]) {
|
|
6201
|
-
toolCalls[tc.index] = {
|
|
9007
|
+
toolCalls[tc.index] = {
|
|
9008
|
+
id: tc.id ?? "",
|
|
9009
|
+
type: "function",
|
|
9010
|
+
function: { name: tc.function?.name ?? "", arguments: "" }
|
|
9011
|
+
};
|
|
6202
9012
|
}
|
|
6203
9013
|
if (tc.function?.arguments)
|
|
6204
9014
|
toolCalls[tc.index].function.arguments += tc.function.arguments;
|
|
@@ -6218,12 +9028,20 @@ function createAILLMFunctions(config, deps) {
|
|
|
6218
9028
|
finishReason
|
|
6219
9029
|
});
|
|
6220
9030
|
} catch (error) {
|
|
6221
|
-
logger18.warn("LLM chat stream failed", {
|
|
9031
|
+
logger18.warn("LLM chat stream failed", {
|
|
9032
|
+
...logContext,
|
|
9033
|
+
durationMs: Date.now() - start,
|
|
9034
|
+
error
|
|
9035
|
+
});
|
|
6222
9036
|
throw error;
|
|
6223
9037
|
}
|
|
6224
9038
|
if (shouldRecord) {
|
|
6225
9039
|
try {
|
|
6226
|
-
const usage = {
|
|
9040
|
+
const usage = {
|
|
9041
|
+
prompt_tokens: 0,
|
|
9042
|
+
completion_tokens: 0,
|
|
9043
|
+
total_tokens: 0
|
|
9044
|
+
};
|
|
6227
9045
|
const record = {
|
|
6228
9046
|
id: `${streamId || "stream"}_${Date.now()}_${recordSeq++}`,
|
|
6229
9047
|
objectId: request.objectId,
|
|
@@ -6260,7 +9078,15 @@ function createAILLMFunctions(config, deps) {
|
|
|
6260
9078
|
});
|
|
6261
9079
|
return ok(records);
|
|
6262
9080
|
} catch (error) {
|
|
6263
|
-
return err(
|
|
9081
|
+
return err(
|
|
9082
|
+
HaiAIError.LLM_HISTORY_FAILED,
|
|
9083
|
+
aiM("ai_llmHistoryFailed", {
|
|
9084
|
+
params: {
|
|
9085
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9086
|
+
}
|
|
9087
|
+
}),
|
|
9088
|
+
error
|
|
9089
|
+
);
|
|
6264
9090
|
}
|
|
6265
9091
|
}
|
|
6266
9092
|
async function listSessions(objectId) {
|
|
@@ -6274,13 +9100,30 @@ function createAILLMFunctions(config, deps) {
|
|
|
6274
9100
|
});
|
|
6275
9101
|
return ok(sessions.filter((item) => !item.deleted));
|
|
6276
9102
|
} catch (error) {
|
|
6277
|
-
return err(
|
|
9103
|
+
return err(
|
|
9104
|
+
HaiAIError.LLM_HISTORY_FAILED,
|
|
9105
|
+
aiM("ai_llmHistoryFailed", {
|
|
9106
|
+
params: {
|
|
9107
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9108
|
+
}
|
|
9109
|
+
}),
|
|
9110
|
+
error
|
|
9111
|
+
);
|
|
6278
9112
|
}
|
|
6279
9113
|
}
|
|
6280
9114
|
const llm = {
|
|
6281
9115
|
chat: chatWithRecord,
|
|
6282
9116
|
chatStream: chatStreamWithRecord,
|
|
6283
|
-
listModels: () =>
|
|
9117
|
+
listModels: async () => {
|
|
9118
|
+
try {
|
|
9119
|
+
return await (await currentProvider()).listModels();
|
|
9120
|
+
} catch {
|
|
9121
|
+
return err(
|
|
9122
|
+
HaiAIError.RESOURCE_OPERATION_FAILED,
|
|
9123
|
+
aiM("ai_resourceOperationFailed")
|
|
9124
|
+
);
|
|
9125
|
+
}
|
|
9126
|
+
},
|
|
6284
9127
|
getHistory,
|
|
6285
9128
|
listSessions,
|
|
6286
9129
|
async ask(question, options) {
|
|
@@ -6334,7 +9177,15 @@ function createAILLMFunctions(config, deps) {
|
|
|
6334
9177
|
const maxRepairs = request.maxRepairs ?? 1;
|
|
6335
9178
|
let lastError = "";
|
|
6336
9179
|
for (let attempt = 0; attempt <= maxRepairs; attempt++) {
|
|
6337
|
-
const messages = attempt === 0 ? baseMessages : [
|
|
9180
|
+
const messages = attempt === 0 ? baseMessages : [
|
|
9181
|
+
...baseMessages,
|
|
9182
|
+
{
|
|
9183
|
+
role: "user",
|
|
9184
|
+
content: aiM("ai_generateObjectRepair", {
|
|
9185
|
+
params: { error: lastError }
|
|
9186
|
+
})
|
|
9187
|
+
}
|
|
9188
|
+
];
|
|
6338
9189
|
const result = await chatWithRecord({
|
|
6339
9190
|
messages,
|
|
6340
9191
|
model: request.model,
|
|
@@ -6342,7 +9193,10 @@ function createAILLMFunctions(config, deps) {
|
|
|
6342
9193
|
tempModel: request.tempModel,
|
|
6343
9194
|
enablePersist: false,
|
|
6344
9195
|
signal: request.signal,
|
|
6345
|
-
response_format: {
|
|
9196
|
+
response_format: {
|
|
9197
|
+
type: "json_schema",
|
|
9198
|
+
json_schema: { name: schemaName, schema: jsonSchema, strict: true }
|
|
9199
|
+
}
|
|
6346
9200
|
});
|
|
6347
9201
|
if (!result.success)
|
|
6348
9202
|
return result;
|
|
@@ -6360,7 +9214,12 @@ function createAILLMFunctions(config, deps) {
|
|
|
6360
9214
|
return ok(validated.data);
|
|
6361
9215
|
lastError = validated.error.message;
|
|
6362
9216
|
}
|
|
6363
|
-
return err(
|
|
9217
|
+
return err(
|
|
9218
|
+
HaiAIError.INVALID_REQUEST,
|
|
9219
|
+
aiM("ai_generateObjectFailed", {
|
|
9220
|
+
params: { error: lastError || "unknown" }
|
|
9221
|
+
})
|
|
9222
|
+
);
|
|
6364
9223
|
}
|
|
6365
9224
|
};
|
|
6366
9225
|
const tools = {
|
|
@@ -6498,25 +9357,25 @@ function createAIMCPFunctions() {
|
|
|
6498
9357
|
}
|
|
6499
9358
|
};
|
|
6500
9359
|
}
|
|
6501
|
-
function createScopedMemory(
|
|
9360
|
+
function createScopedMemory(core32, binding) {
|
|
6502
9361
|
const { objectId, scope } = binding;
|
|
6503
9362
|
const accessScope = { objectId, scope };
|
|
6504
9363
|
return {
|
|
6505
|
-
extract: (messages, options) =>
|
|
6506
|
-
recall: (query, options) =>
|
|
6507
|
-
injectMemories: (messages, options) =>
|
|
6508
|
-
add: (entry) =>
|
|
6509
|
-
update: (memoryId, updates) =>
|
|
6510
|
-
get: (memoryId) =>
|
|
6511
|
-
remove: (memoryId) =>
|
|
6512
|
-
list: (options) =>
|
|
6513
|
-
listPage: (options) =>
|
|
6514
|
-
clear: (options) =>
|
|
9364
|
+
extract: (messages, options) => core32.extract(messages, { ...options, objectId, scope }),
|
|
9365
|
+
recall: (query, options) => core32.recall(query, { ...options, objectId, scope }),
|
|
9366
|
+
injectMemories: (messages, options) => core32.injectMemories(messages, { ...options, objectId, scope }),
|
|
9367
|
+
add: (entry) => core32.add({ ...entry, objectId, scope }),
|
|
9368
|
+
update: (memoryId, updates) => core32.update(memoryId, updates, accessScope),
|
|
9369
|
+
get: (memoryId) => core32.get(memoryId, accessScope),
|
|
9370
|
+
remove: (memoryId) => core32.remove(memoryId, accessScope),
|
|
9371
|
+
list: (options) => core32.list({ ...options, objectId, scope }),
|
|
9372
|
+
listPage: (options) => core32.listPage({ ...options, objectId, scope }),
|
|
9373
|
+
clear: (options) => core32.clear({ ...options, objectId, scope })
|
|
6515
9374
|
};
|
|
6516
9375
|
}
|
|
6517
|
-
function createMemoryOperations(
|
|
9376
|
+
function createMemoryOperations(core32) {
|
|
6518
9377
|
return {
|
|
6519
|
-
...
|
|
9378
|
+
...core32,
|
|
6520
9379
|
/**
|
|
6521
9380
|
* 清空记忆(空过滤保护)。
|
|
6522
9381
|
*
|
|
@@ -6528,15 +9387,15 @@ function createMemoryOperations(core33) {
|
|
|
6528
9387
|
if (!hasFilter) {
|
|
6529
9388
|
return err(HaiAIError.MEMORY_STORE_FAILED, aiM("ai_memoryClearRequiresFilter"));
|
|
6530
9389
|
}
|
|
6531
|
-
return
|
|
9390
|
+
return core32.clear(options);
|
|
6532
9391
|
},
|
|
6533
|
-
scoped: (binding) => createScopedMemory(
|
|
9392
|
+
scoped: (binding) => createScopedMemory(core32, binding),
|
|
6534
9393
|
admin: {
|
|
6535
9394
|
async clearAll(options) {
|
|
6536
9395
|
if (options?.confirm !== true) {
|
|
6537
9396
|
return err(HaiAIError.MEMORY_STORE_FAILED, aiM("ai_memoryClearAllUnconfirmed"));
|
|
6538
9397
|
}
|
|
6539
|
-
return
|
|
9398
|
+
return core32.clear();
|
|
6540
9399
|
}
|
|
6541
9400
|
}
|
|
6542
9401
|
};
|
|
@@ -6887,7 +9746,7 @@ function buildMem0Config(deps) {
|
|
|
6887
9746
|
const vectorResolved = buildVectorStoreConfig(deps);
|
|
6888
9747
|
if (!vectorResolved.success)
|
|
6889
9748
|
return vectorResolved;
|
|
6890
|
-
if (core.module.requiresSharedState(
|
|
9749
|
+
if (core.module.requiresSharedState(process2.env) && !vectorResolved.data.persistent)
|
|
6891
9750
|
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_memorySharedBackendRequired"));
|
|
6892
9751
|
return ok({
|
|
6893
9752
|
vectorProvider: vectorResolved.data.provider,
|
|
@@ -7099,16 +9958,16 @@ async function createMem0OssMemoryOperations(deps) {
|
|
|
7099
9958
|
const sdk = await import('@qdrant/js-client-rest').catch(() => void 0);
|
|
7100
9959
|
if (sdk && typeof sdk.QdrantClient?.prototype?.search === "function") {
|
|
7101
9960
|
const url = deps.vectorBackend?.url ?? "";
|
|
7102
|
-
const
|
|
9961
|
+
const client2 = new sdk.QdrantClient({
|
|
7103
9962
|
url,
|
|
7104
9963
|
port: Number(new URL(url).port) || 6333,
|
|
7105
9964
|
apiKey: deps.vectorBackend?.apiKey
|
|
7106
9965
|
});
|
|
7107
|
-
const upsert =
|
|
7108
|
-
const remove =
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
configResult.data.config.vectorStore.config.client =
|
|
9966
|
+
const upsert = client2.upsert.bind(client2);
|
|
9967
|
+
const remove = client2.delete.bind(client2);
|
|
9968
|
+
client2.upsert = (collection, options) => upsert(collection, { ...options, wait: true });
|
|
9969
|
+
client2.delete = (collection, options) => remove(collection, { ...options, wait: true });
|
|
9970
|
+
configResult.data.config.vectorStore.config.client = client2;
|
|
7112
9971
|
} else {
|
|
7113
9972
|
const failure = err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_memoryBackendSdkIncompatible"));
|
|
7114
9973
|
if (!failure.success)
|
|
@@ -8451,7 +11310,7 @@ function createRerankOperations(config) {
|
|
|
8451
11310
|
};
|
|
8452
11311
|
}
|
|
8453
11312
|
var logger28 = core.logger.child({ module: "ai", scope: "retrieval" });
|
|
8454
|
-
function createRetrievalOperations(embeddingOps, storeProvider, sourceStore, rerankOps) {
|
|
11313
|
+
function createRetrievalOperations(embeddingOps, storeProvider, sourceStore, rerankOps, registry) {
|
|
8455
11314
|
const vectorStoreCache = /* @__PURE__ */ new Map();
|
|
8456
11315
|
function getVectorStore(collection) {
|
|
8457
11316
|
let store = vectorStoreCache.get(collection);
|
|
@@ -8472,6 +11331,10 @@ function createRetrievalOperations(embeddingOps, storeProvider, sourceStore, rer
|
|
|
8472
11331
|
* @returns `ok(undefined)` 注册成功;`err` 含错误码 `RETRIEVAL_FAILED`(重复注册)
|
|
8473
11332
|
*/
|
|
8474
11333
|
async addSource(source) {
|
|
11334
|
+
if (registry) {
|
|
11335
|
+
const result = await registry.operations.register(source);
|
|
11336
|
+
return result.success ? ok(void 0) : result;
|
|
11337
|
+
}
|
|
8475
11338
|
const existing = await sourceStore.get(source.id);
|
|
8476
11339
|
if (existing) {
|
|
8477
11340
|
return err(HaiAIError.RETRIEVAL_FAILED, aiM("ai_internalError", { params: { error: `Source '${source.id}' already exists` } }));
|
|
@@ -8489,6 +11352,8 @@ function createRetrievalOperations(embeddingOps, storeProvider, sourceStore, rer
|
|
|
8489
11352
|
* @returns `ok(undefined)` 删除成功;`err` 含错误码 `RETRIEVAL_SOURCE_NOT_FOUND`
|
|
8490
11353
|
*/
|
|
8491
11354
|
async removeSource(sourceId) {
|
|
11355
|
+
if (registry)
|
|
11356
|
+
return registry.operations.remove(sourceId);
|
|
8492
11357
|
const existing = await sourceStore.get(sourceId);
|
|
8493
11358
|
if (!existing) {
|
|
8494
11359
|
return err(HaiAIError.RETRIEVAL_SOURCE_NOT_FOUND, aiM("ai_internalError", { params: { error: `Source '${sourceId}' not found` } }));
|
|
@@ -8503,7 +11368,7 @@ function createRetrievalOperations(embeddingOps, storeProvider, sourceStore, rer
|
|
|
8503
11368
|
* @returns 全部检索源数组(无源时返回空数组)
|
|
8504
11369
|
*/
|
|
8505
11370
|
async listSources() {
|
|
8506
|
-
return sourceStore.query({});
|
|
11371
|
+
return registry ? registry.values() : sourceStore.query({});
|
|
8507
11372
|
},
|
|
8508
11373
|
/**
|
|
8509
11374
|
* 执行向量检索
|
|
@@ -8518,13 +11383,13 @@ function createRetrievalOperations(embeddingOps, storeProvider, sourceStore, rer
|
|
|
8518
11383
|
*/
|
|
8519
11384
|
async retrieve(request) {
|
|
8520
11385
|
const startTime = Date.now();
|
|
8521
|
-
const targetSources = await sourceStore.query(
|
|
8522
|
-
request.sources?.length ? { where: { id: { $in: request.sources } } } : {}
|
|
8523
|
-
);
|
|
8524
|
-
if (targetSources.length === 0) {
|
|
8525
|
-
return err(HaiAIError.RETRIEVAL_SOURCE_NOT_FOUND, aiM("ai_internalError", { params: { error: "No retrieval sources configured" } }));
|
|
8526
|
-
}
|
|
8527
11386
|
try {
|
|
11387
|
+
const targetSources = registry ? (await registry.values()).filter((source) => !request.sources?.length || request.sources.includes(source.id)) : await sourceStore.query(
|
|
11388
|
+
request.sources?.length ? { where: { id: { $in: request.sources } } } : {}
|
|
11389
|
+
);
|
|
11390
|
+
if (targetSources.length === 0) {
|
|
11391
|
+
return err(HaiAIError.RETRIEVAL_SOURCE_NOT_FOUND, aiM("ai_internalError", { params: { error: "No retrieval sources configured" } }));
|
|
11392
|
+
}
|
|
8528
11393
|
const embedResult = await embeddingOps.embedText(request.query);
|
|
8529
11394
|
if (!embedResult.success) {
|
|
8530
11395
|
return err(HaiAIError.RETRIEVAL_FAILED, aiM("ai_internalError", { params: { error: "Failed to embed query" } }), embedResult.error);
|
|
@@ -8703,38 +11568,207 @@ function createTokenOperations(config) {
|
|
|
8703
11568
|
}
|
|
8704
11569
|
|
|
8705
11570
|
// src/ai-functions.ts
|
|
8706
|
-
var logger30 = core.logger.child({ module: "ai", scope: "functions" });
|
|
8707
11571
|
async function createAISubsystems(config, deps) {
|
|
8708
11572
|
const { storeProvider, datapipe: datapipeDep } = deps;
|
|
8709
11573
|
const memoryParsed = MemoryConfigSchema.parse(config.memory ?? {});
|
|
8710
11574
|
const useMem0Oss = memoryParsed.provider === "mem0";
|
|
8711
|
-
const chatRecordStore = storeProvider.createRelStore(
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
const
|
|
8716
|
-
|
|
11575
|
+
const chatRecordStore = storeProvider.createRelStore(
|
|
11576
|
+
"hai_ai_chat_records",
|
|
11577
|
+
{ hasObjectId: true, hasSessionId: true }
|
|
11578
|
+
);
|
|
11579
|
+
const sessionStore = storeProvider.createRelStore(
|
|
11580
|
+
"hai_ai_sessions",
|
|
11581
|
+
{ hasObjectId: true }
|
|
11582
|
+
);
|
|
11583
|
+
const sourceStore = storeProvider.createRelStore(
|
|
11584
|
+
"hai_ai_retrieval_sources"
|
|
11585
|
+
);
|
|
11586
|
+
const memoryStore = useMem0Oss ? null : storeProvider.createRelStore("hai_ai_memory", {
|
|
11587
|
+
hasObjectId: true,
|
|
11588
|
+
hasScopeIndex: true
|
|
11589
|
+
});
|
|
11590
|
+
const contextStore = storeProvider.createRelStore(
|
|
11591
|
+
"hai_ai_context",
|
|
11592
|
+
{ hasObjectId: true, hasSessionId: true }
|
|
11593
|
+
);
|
|
11594
|
+
const personaStore = storeProvider.createRelStore(
|
|
11595
|
+
"hai_ai_persona",
|
|
11596
|
+
{ hasObjectId: true }
|
|
11597
|
+
);
|
|
8717
11598
|
const a2aStores = config.a2a ? createA2AStores(storeProvider) : null;
|
|
8718
11599
|
const memoryVectorStore = useMem0Oss ? null : storeProvider.createVectorStore("hai_ai_memory_vectors");
|
|
11600
|
+
let registriesOpen = true;
|
|
11601
|
+
const isOpen = () => registriesOpen;
|
|
11602
|
+
const llmModels = createResourceRegistry(
|
|
11603
|
+
storeProvider.createRelStore("hai_ai_llm_models"),
|
|
11604
|
+
ModelEntrySchema,
|
|
11605
|
+
config.llm.models ?? [],
|
|
11606
|
+
isOpen
|
|
11607
|
+
);
|
|
11608
|
+
const audioModels = createResourceRegistry(
|
|
11609
|
+
storeProvider.createRelStore("hai_ai_audio_models"),
|
|
11610
|
+
AudioModelEntrySchema,
|
|
11611
|
+
config.audio?.models ?? [],
|
|
11612
|
+
isOpen
|
|
11613
|
+
);
|
|
11614
|
+
const imageModels = createResourceRegistry(
|
|
11615
|
+
storeProvider.createRelStore("hai_ai_image_models"),
|
|
11616
|
+
ImageModelEntrySchema,
|
|
11617
|
+
config.image?.models ?? [],
|
|
11618
|
+
isOpen
|
|
11619
|
+
);
|
|
11620
|
+
const mcpServers = createResourceRegistry(
|
|
11621
|
+
storeProvider.createRelStore("hai_ai_mcp_servers"),
|
|
11622
|
+
MCPExternalServerSchema,
|
|
11623
|
+
config.mcp?.servers ?? [],
|
|
11624
|
+
isOpen
|
|
11625
|
+
);
|
|
11626
|
+
const retrievalSources = createResourceRegistry(
|
|
11627
|
+
sourceStore,
|
|
11628
|
+
RetrievalSourceSchema,
|
|
11629
|
+
config.retrieval?.sources ?? [],
|
|
11630
|
+
isOpen
|
|
11631
|
+
);
|
|
11632
|
+
async function llmConfig() {
|
|
11633
|
+
return {
|
|
11634
|
+
...config,
|
|
11635
|
+
llm: { ...config.llm, models: await llmModels.values() }
|
|
11636
|
+
};
|
|
11637
|
+
}
|
|
11638
|
+
async function audioConfig() {
|
|
11639
|
+
return {
|
|
11640
|
+
...config,
|
|
11641
|
+
audio: AudioConfigSchema.parse({
|
|
11642
|
+
...config.audio,
|
|
11643
|
+
models: await audioModels.values()
|
|
11644
|
+
})
|
|
11645
|
+
};
|
|
11646
|
+
}
|
|
11647
|
+
async function imageConfig() {
|
|
11648
|
+
return {
|
|
11649
|
+
...config,
|
|
11650
|
+
image: ImageConfigSchema.parse({
|
|
11651
|
+
...config.image,
|
|
11652
|
+
models: await imageModels.values()
|
|
11653
|
+
})
|
|
11654
|
+
};
|
|
11655
|
+
}
|
|
11656
|
+
async function mcpLookup(ids) {
|
|
11657
|
+
const servers = [];
|
|
11658
|
+
for (const id of ids) {
|
|
11659
|
+
const entry = await mcpServers.read(id);
|
|
11660
|
+
if (!entry)
|
|
11661
|
+
throw new Error("MCP resource missing");
|
|
11662
|
+
const server = entry.value;
|
|
11663
|
+
if (server.transport === "stdio") {
|
|
11664
|
+
servers.push({
|
|
11665
|
+
name: server.name,
|
|
11666
|
+
command: server.command,
|
|
11667
|
+
args: server.args,
|
|
11668
|
+
env: Object.entries(resolveEnvironment(server.environmentRefs)).map(
|
|
11669
|
+
([name, value]) => ({ name, value })
|
|
11670
|
+
)
|
|
11671
|
+
});
|
|
11672
|
+
} else {
|
|
11673
|
+
servers.push({
|
|
11674
|
+
type: server.transport,
|
|
11675
|
+
name: server.name,
|
|
11676
|
+
url: server.url,
|
|
11677
|
+
headers: Object.entries(resolveEnvironment(server.headerEnv)).map(
|
|
11678
|
+
([name, value]) => ({ name, value })
|
|
11679
|
+
)
|
|
11680
|
+
});
|
|
11681
|
+
}
|
|
11682
|
+
}
|
|
11683
|
+
return servers;
|
|
11684
|
+
}
|
|
11685
|
+
const acp = createACPOperations(
|
|
11686
|
+
storeProvider,
|
|
11687
|
+
config,
|
|
11688
|
+
deps.acpAdapters,
|
|
11689
|
+
llmConfig,
|
|
11690
|
+
mcpLookup
|
|
11691
|
+
);
|
|
8719
11692
|
await storeProvider.initialize();
|
|
8720
|
-
const llmFunctions = createAILLMFunctions(config, {
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
const
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
11693
|
+
const llmFunctions = createAILLMFunctions(config, {
|
|
11694
|
+
recordStore: chatRecordStore,
|
|
11695
|
+
sessionStore,
|
|
11696
|
+
resolveConfig: llmConfig
|
|
11697
|
+
});
|
|
11698
|
+
const llm = Object.assign(llmFunctions.llm, {
|
|
11699
|
+
models: llmModels.operations
|
|
11700
|
+
});
|
|
11701
|
+
const mcp = Object.assign(createAIMCPFunctions(), {
|
|
11702
|
+
servers: mcpServers.operations
|
|
11703
|
+
});
|
|
11704
|
+
async function resourceCall(operation) {
|
|
11705
|
+
try {
|
|
11706
|
+
return await operation();
|
|
11707
|
+
} catch {
|
|
11708
|
+
return err(
|
|
11709
|
+
HaiAIError.RESOURCE_OPERATION_FAILED,
|
|
11710
|
+
aiM("ai_resourceOperationFailed")
|
|
11711
|
+
);
|
|
11712
|
+
}
|
|
8730
11713
|
}
|
|
11714
|
+
const embedding = {
|
|
11715
|
+
embed: (request) => resourceCall(
|
|
11716
|
+
async () => createEmbeddingOperations(await llmConfig()).embed(request)
|
|
11717
|
+
),
|
|
11718
|
+
embedText: (text) => resourceCall(
|
|
11719
|
+
async () => createEmbeddingOperations(await llmConfig()).embedText(text)
|
|
11720
|
+
),
|
|
11721
|
+
embedBatch: (texts) => resourceCall(
|
|
11722
|
+
async () => createEmbeddingOperations(await llmConfig()).embedBatch(texts)
|
|
11723
|
+
)
|
|
11724
|
+
};
|
|
11725
|
+
const reasoning = {
|
|
11726
|
+
run: (query, options) => resourceCall(async () => createReasoningOperations(await llmConfig(), llm).run(query, options)),
|
|
11727
|
+
async *runStream(query, options) {
|
|
11728
|
+
yield* createReasoningOperations(await llmConfig(), llm).runStream(query, options);
|
|
11729
|
+
}
|
|
11730
|
+
};
|
|
11731
|
+
const rerank = {
|
|
11732
|
+
rerank: (request) => resourceCall(
|
|
11733
|
+
async () => createRerankOperations(await llmConfig()).rerank(request)
|
|
11734
|
+
),
|
|
11735
|
+
rerankTexts: (query, texts, topN) => resourceCall(
|
|
11736
|
+
async () => createRerankOperations(await llmConfig()).rerankTexts(
|
|
11737
|
+
query,
|
|
11738
|
+
texts,
|
|
11739
|
+
topN
|
|
11740
|
+
)
|
|
11741
|
+
)
|
|
11742
|
+
};
|
|
11743
|
+
const retrieval = Object.assign(
|
|
11744
|
+
createRetrievalOperations(
|
|
11745
|
+
embedding,
|
|
11746
|
+
storeProvider,
|
|
11747
|
+
sourceStore,
|
|
11748
|
+
rerank,
|
|
11749
|
+
retrievalSources
|
|
11750
|
+
),
|
|
11751
|
+
{ sources: retrievalSources.operations }
|
|
11752
|
+
);
|
|
8731
11753
|
const rag = createRagOperations(llm, retrieval);
|
|
8732
11754
|
const knowledgeParsed = KnowledgeConfigSchema.parse(config.knowledge ?? {});
|
|
8733
11755
|
const knowledgeStore = storeProvider.createKnowledgeStore?.();
|
|
8734
|
-
const knowledge = createKnowledgeOperations(
|
|
8735
|
-
|
|
11756
|
+
const knowledge = createKnowledgeOperations(
|
|
11757
|
+
knowledgeParsed,
|
|
11758
|
+
llm,
|
|
11759
|
+
embedding,
|
|
11760
|
+
datapipeDep,
|
|
11761
|
+
knowledgeStore
|
|
11762
|
+
);
|
|
11763
|
+
const memoryCore = memoryStore && memoryVectorStore ? createNativeMemoryOperations(
|
|
11764
|
+
memoryParsed,
|
|
11765
|
+
llm,
|
|
11766
|
+
embedding,
|
|
11767
|
+
memoryStore,
|
|
11768
|
+
memoryVectorStore
|
|
11769
|
+
) : await createMem0OssMemoryOperations({
|
|
8736
11770
|
config: memoryParsed,
|
|
8737
|
-
aiConfig:
|
|
11771
|
+
aiConfig: await llmConfig(),
|
|
8738
11772
|
llm,
|
|
8739
11773
|
collectionName: "hai_ai_memory",
|
|
8740
11774
|
embeddingDims: config.embedding?.dimensions,
|
|
@@ -8745,20 +11779,109 @@ async function createAISubsystems(config, deps) {
|
|
|
8745
11779
|
const summaryParsed = SummaryConfigSchema.parse(config.summary ?? {});
|
|
8746
11780
|
const compressParsed = CompressConfigSchema.parse(config.compress ?? {});
|
|
8747
11781
|
const token = createTokenOperations(tokenParsed);
|
|
8748
|
-
const summary =
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
summary
|
|
8756
|
-
|
|
11782
|
+
const summary = {
|
|
11783
|
+
generate: (messages, options) => resourceCall(async () => createSummaryOperations((await llmConfig()).llm, llm, token, summaryParsed).generate(messages, options)),
|
|
11784
|
+
summarize: (messages, options) => resourceCall(async () => createSummaryOperations((await llmConfig()).llm, llm, token, summaryParsed).summarize(messages, options))
|
|
11785
|
+
};
|
|
11786
|
+
const compress = createCompressOperations(
|
|
11787
|
+
compressParsed,
|
|
11788
|
+
token,
|
|
11789
|
+
summary,
|
|
11790
|
+
config.llm.maxTokens ?? 4096
|
|
11791
|
+
);
|
|
11792
|
+
const context = createContextOperations(
|
|
11793
|
+
compressParsed,
|
|
11794
|
+
token,
|
|
11795
|
+
compress,
|
|
11796
|
+
contextStore,
|
|
11797
|
+
sessionStore,
|
|
11798
|
+
{
|
|
11799
|
+
llm,
|
|
11800
|
+
memory: memory ?? void 0,
|
|
11801
|
+
rag: rag ?? void 0,
|
|
11802
|
+
reasoning: reasoning ?? void 0,
|
|
11803
|
+
summary
|
|
11804
|
+
}
|
|
11805
|
+
);
|
|
8757
11806
|
const persona = createPersonaOperations(personaStore);
|
|
8758
|
-
const file =
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
11807
|
+
const file = {
|
|
11808
|
+
parse: (request) => resourceCall(async () => createFileOperations(await llmConfig(), llm).parse(request)),
|
|
11809
|
+
parseText: (content, filename) => resourceCall(async () => createFileOperations(await llmConfig(), llm).parseText(content, filename))
|
|
11810
|
+
};
|
|
11811
|
+
let audioSnapshot = createAudioOperations(await audioConfig());
|
|
11812
|
+
async function currentAudio2() {
|
|
11813
|
+
audioSnapshot = createAudioOperations(await audioConfig());
|
|
11814
|
+
return audioSnapshot;
|
|
11815
|
+
}
|
|
11816
|
+
const audio = {
|
|
11817
|
+
models: {
|
|
11818
|
+
list: () => resourceCall(async () => {
|
|
11819
|
+
await currentAudio2();
|
|
11820
|
+
return audioModels.operations.list();
|
|
11821
|
+
}),
|
|
11822
|
+
get: (id) => resourceCall(async () => {
|
|
11823
|
+
await currentAudio2();
|
|
11824
|
+
return audioModels.operations.get(id);
|
|
11825
|
+
}),
|
|
11826
|
+
register: (input) => resourceCall(async () => {
|
|
11827
|
+
const result = await audioModels.operations.register(input);
|
|
11828
|
+
if (result.success)
|
|
11829
|
+
await currentAudio2();
|
|
11830
|
+
return result;
|
|
11831
|
+
}),
|
|
11832
|
+
update: (input, version) => resourceCall(async () => {
|
|
11833
|
+
const result = await audioModels.operations.update(input, version);
|
|
11834
|
+
if (result.success)
|
|
11835
|
+
await currentAudio2();
|
|
11836
|
+
return result;
|
|
11837
|
+
}),
|
|
11838
|
+
remove: (id, version) => resourceCall(async () => {
|
|
11839
|
+
const result = await audioModels.operations.remove(id, version);
|
|
11840
|
+
if (result.success)
|
|
11841
|
+
await currentAudio2();
|
|
11842
|
+
return result;
|
|
11843
|
+
})
|
|
11844
|
+
},
|
|
11845
|
+
transcribe: (request) => resourceCall(async () => (await currentAudio2()).transcribe(request)),
|
|
11846
|
+
synthesize: (request) => resourceCall(async () => (await currentAudio2()).synthesize(request)),
|
|
11847
|
+
async *transcribeStream(request) {
|
|
11848
|
+
yield* (await currentAudio2()).transcribeStream(request);
|
|
11849
|
+
},
|
|
11850
|
+
async *synthesizeStream(request) {
|
|
11851
|
+
yield* (await currentAudio2()).synthesizeStream(request);
|
|
11852
|
+
},
|
|
11853
|
+
getCapabilities: (request) => audioSnapshot.getCapabilities(request)
|
|
11854
|
+
};
|
|
11855
|
+
const image = {
|
|
11856
|
+
models: imageModels.operations,
|
|
11857
|
+
generate: (request) => resourceCall(
|
|
11858
|
+
async () => createImageOperations(await imageConfig()).generate(request)
|
|
11859
|
+
)
|
|
11860
|
+
};
|
|
11861
|
+
return {
|
|
11862
|
+
closeRegistries: () => {
|
|
11863
|
+
registriesOpen = false;
|
|
11864
|
+
},
|
|
11865
|
+
a2aStores,
|
|
11866
|
+
llm,
|
|
11867
|
+
acp,
|
|
11868
|
+
mcp,
|
|
11869
|
+
embedding,
|
|
11870
|
+
reasoning,
|
|
11871
|
+
rerank,
|
|
11872
|
+
retrieval,
|
|
11873
|
+
rag,
|
|
11874
|
+
knowledge,
|
|
11875
|
+
memory,
|
|
11876
|
+
persona,
|
|
11877
|
+
token,
|
|
11878
|
+
summary,
|
|
11879
|
+
compress,
|
|
11880
|
+
context,
|
|
11881
|
+
file,
|
|
11882
|
+
audio,
|
|
11883
|
+
image
|
|
11884
|
+
};
|
|
8762
11885
|
}
|
|
8763
11886
|
var WRITE_LEASE_MS = 3e5;
|
|
8764
11887
|
var CAS_ATTEMPTS = 12;
|
|
@@ -9077,7 +12200,7 @@ function documentInfo(document) {
|
|
|
9077
12200
|
}
|
|
9078
12201
|
|
|
9079
12202
|
// src/store/providers/ai-store-provider-db.ts
|
|
9080
|
-
var
|
|
12203
|
+
var logger30 = core.logger.child({ module: "ai", scope: "store-provider-db" });
|
|
9081
12204
|
function requireSqlSuccess(result) {
|
|
9082
12205
|
if (!result.success)
|
|
9083
12206
|
throw new Error(result.error.message, { cause: result.error });
|
|
@@ -9596,7 +12719,7 @@ function createDbStoreProvider(deps) {
|
|
|
9596
12719
|
if (result.status === "rejected")
|
|
9597
12720
|
throw result.reason;
|
|
9598
12721
|
}
|
|
9599
|
-
|
|
12722
|
+
logger30.debug("DB store provider initialized", { tableCount: relStores.size });
|
|
9600
12723
|
}
|
|
9601
12724
|
},
|
|
9602
12725
|
async close() {
|
|
@@ -9827,10 +12950,11 @@ function createMemoryStoreProvider() {
|
|
|
9827
12950
|
}
|
|
9828
12951
|
|
|
9829
12952
|
// src/ai-main.ts
|
|
9830
|
-
var
|
|
12953
|
+
var logger31 = core.logger.child({ module: "ai", scope: "main" });
|
|
9831
12954
|
var initInProgress = false;
|
|
9832
12955
|
var currentConfig = null;
|
|
9833
12956
|
var currentLLM = null;
|
|
12957
|
+
var currentACP = null;
|
|
9834
12958
|
var currentMCP = null;
|
|
9835
12959
|
var currentEmbedding = null;
|
|
9836
12960
|
var currentReasoning = null;
|
|
@@ -9850,12 +12974,14 @@ var currentImage = null;
|
|
|
9850
12974
|
var currentA2AConfig;
|
|
9851
12975
|
var currentA2AImpl = null;
|
|
9852
12976
|
var currentA2AStores = null;
|
|
12977
|
+
var currentRegistriesClose = null;
|
|
9853
12978
|
var currentStoreProvider = null;
|
|
9854
12979
|
var notInitialized = core.module.createNotInitializedKit(
|
|
9855
12980
|
HaiAIError.NOT_INITIALIZED,
|
|
9856
12981
|
() => aiM("ai_notInitialized")
|
|
9857
12982
|
);
|
|
9858
12983
|
var notInitializedLLM = {
|
|
12984
|
+
models: notInitialized.proxy(),
|
|
9859
12985
|
chat: () => Promise.resolve(notInitialized.result()),
|
|
9860
12986
|
async *chatStream() {
|
|
9861
12987
|
throw notInitialized.error();
|
|
@@ -9869,7 +12995,16 @@ var notInitializedLLM = {
|
|
|
9869
12995
|
},
|
|
9870
12996
|
generateObject: () => Promise.resolve(notInitialized.result())
|
|
9871
12997
|
};
|
|
12998
|
+
var notInitializedACP = {
|
|
12999
|
+
agents: notInitialized.proxy(),
|
|
13000
|
+
sessions: notInitialized.proxy(),
|
|
13001
|
+
workspaces: notInitialized.proxy(),
|
|
13002
|
+
connect: () => Promise.resolve(notInitialized.result()),
|
|
13003
|
+
close: async () => {
|
|
13004
|
+
}
|
|
13005
|
+
};
|
|
9872
13006
|
var notInitializedMCP = {
|
|
13007
|
+
servers: notInitialized.proxy(),
|
|
9873
13008
|
registerTool: () => notInitialized.result(),
|
|
9874
13009
|
registerResource: () => notInitialized.result(),
|
|
9875
13010
|
registerPrompt: () => notInitialized.result(),
|
|
@@ -9885,6 +13020,7 @@ var notInitializedReasoning = {
|
|
|
9885
13020
|
}
|
|
9886
13021
|
};
|
|
9887
13022
|
var notInitializedRetrieval = {
|
|
13023
|
+
sources: notInitialized.proxy(),
|
|
9888
13024
|
addSource: () => Promise.resolve(notInitialized.result()),
|
|
9889
13025
|
removeSource: () => Promise.resolve(notInitialized.result()),
|
|
9890
13026
|
listSources: () => Promise.resolve([]),
|
|
@@ -9919,6 +13055,7 @@ var notInitializedContext = {
|
|
|
9919
13055
|
var notInitializedRerank = notInitialized.proxy();
|
|
9920
13056
|
var notInitializedFile = notInitialized.proxy();
|
|
9921
13057
|
var notInitializedAudio = {
|
|
13058
|
+
models: notInitialized.proxy(),
|
|
9922
13059
|
transcribe: () => Promise.resolve(notInitialized.result()),
|
|
9923
13060
|
async *transcribeStream() {
|
|
9924
13061
|
throw notInitialized.error();
|
|
@@ -9930,6 +13067,7 @@ var notInitializedAudio = {
|
|
|
9930
13067
|
getCapabilities: () => notInitialized.result()
|
|
9931
13068
|
};
|
|
9932
13069
|
var notInitializedImage = {
|
|
13070
|
+
models: notInitialized.proxy(),
|
|
9933
13071
|
generate: () => Promise.resolve(notInitialized.result())
|
|
9934
13072
|
};
|
|
9935
13073
|
var a2aLazyOperations = createA2ALazyProxy({
|
|
@@ -9945,7 +13083,11 @@ var a2aLazyOperations = createA2ALazyProxy({
|
|
|
9945
13083
|
async function resetAllState() {
|
|
9946
13084
|
const storeProvider = currentStoreProvider;
|
|
9947
13085
|
await currentA2AImpl?.close();
|
|
13086
|
+
await currentACP?.close();
|
|
9948
13087
|
currentLLM = null;
|
|
13088
|
+
currentRegistriesClose?.();
|
|
13089
|
+
currentRegistriesClose = null;
|
|
13090
|
+
currentACP = null;
|
|
9949
13091
|
currentMCP = null;
|
|
9950
13092
|
currentEmbedding = null;
|
|
9951
13093
|
currentReasoning = null;
|
|
@@ -9971,7 +13113,7 @@ async function resetAllState() {
|
|
|
9971
13113
|
try {
|
|
9972
13114
|
await storeProvider.close();
|
|
9973
13115
|
} catch (error) {
|
|
9974
|
-
|
|
13116
|
+
logger31.error("AI store provider close failed", { error });
|
|
9975
13117
|
}
|
|
9976
13118
|
}
|
|
9977
13119
|
}
|
|
@@ -9987,22 +13129,25 @@ var streamOperations = {
|
|
|
9987
13129
|
};
|
|
9988
13130
|
var ai = {
|
|
9989
13131
|
async init(config, options) {
|
|
9990
|
-
if (Object.keys(
|
|
13132
|
+
if (Object.keys(process2.env).some((name) => /^HAI_AI_LLM_(?:MODELS_\d+_)?API$/.test(name)))
|
|
9991
13133
|
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_llmProtocolRenamed"));
|
|
9992
13134
|
if (initInProgress) {
|
|
9993
|
-
|
|
13135
|
+
logger31.warn("AI init already in progress, skipping concurrent call");
|
|
9994
13136
|
return err(HaiAIError.INIT_IN_PROGRESS, aiM("ai_initInProgress"));
|
|
9995
13137
|
}
|
|
9996
13138
|
initInProgress = true;
|
|
13139
|
+
let closingPrevious = false;
|
|
9997
13140
|
try {
|
|
9998
13141
|
if (currentConfig) {
|
|
9999
|
-
|
|
13142
|
+
logger31.warn("AI module is already initialized, reinitializing");
|
|
13143
|
+
closingPrevious = true;
|
|
10000
13144
|
await ai.close();
|
|
13145
|
+
closingPrevious = false;
|
|
10001
13146
|
}
|
|
10002
|
-
|
|
13147
|
+
logger31.info("Initializing AI module");
|
|
10003
13148
|
const parseResult = AIConfigSchema.safeParse(config ?? {});
|
|
10004
13149
|
if (!parseResult.success) {
|
|
10005
|
-
|
|
13150
|
+
logger31.error("AI config validation failed", { error: parseResult.error.message });
|
|
10006
13151
|
return err(
|
|
10007
13152
|
HaiAIError.CONFIGURATION_ERROR,
|
|
10008
13153
|
aiM("ai_configError", { params: { error: parseResult.error.message } }),
|
|
@@ -10017,16 +13162,19 @@ var ai = {
|
|
|
10017
13162
|
storeProvider = createDbStoreProviderFromModules();
|
|
10018
13163
|
} else {
|
|
10019
13164
|
storeProvider = createMemoryStoreProvider();
|
|
10020
|
-
|
|
13165
|
+
logger31.warn("AI is using the ephemeral in-memory store; initialize reldb and vecdb before ai.init() for persistent data");
|
|
10021
13166
|
}
|
|
10022
|
-
if (core.module.requiresSharedState(
|
|
13167
|
+
if (core.module.requiresSharedState(process2.env) && storeProvider.stateScope !== "shared")
|
|
10023
13168
|
return err(HaiAIError.CONFIGURATION_ERROR, aiM("ai_sharedBackendRequired"));
|
|
10024
13169
|
currentStoreProvider = storeProvider;
|
|
10025
13170
|
const subs = await createAISubsystems(parsed, {
|
|
10026
13171
|
storeProvider,
|
|
10027
|
-
datapipe
|
|
13172
|
+
datapipe,
|
|
13173
|
+
acpAdapters: options?.acpAdapters
|
|
10028
13174
|
});
|
|
13175
|
+
currentRegistriesClose = subs.closeRegistries;
|
|
10029
13176
|
currentLLM = subs.llm;
|
|
13177
|
+
currentACP = subs.acp;
|
|
10030
13178
|
currentMCP = subs.mcp;
|
|
10031
13179
|
currentEmbedding = subs.embedding;
|
|
10032
13180
|
currentReasoning = subs.reasoning;
|
|
@@ -10046,11 +13194,17 @@ var ai = {
|
|
|
10046
13194
|
currentA2AConfig = parsed.a2a;
|
|
10047
13195
|
currentA2AStores = subs.a2aStores;
|
|
10048
13196
|
currentConfig = parsed;
|
|
10049
|
-
|
|
13197
|
+
logger31.info("AI module initialized", { model: parsed.llm?.model });
|
|
10050
13198
|
return ok(void 0);
|
|
10051
13199
|
} catch (error) {
|
|
10052
|
-
|
|
10053
|
-
|
|
13200
|
+
if (!closingPrevious) {
|
|
13201
|
+
try {
|
|
13202
|
+
await resetAllState();
|
|
13203
|
+
} catch (cleanupError) {
|
|
13204
|
+
logger31.error("AI initialization cleanup failed", { error: cleanupError });
|
|
13205
|
+
}
|
|
13206
|
+
}
|
|
13207
|
+
logger31.error("AI module initialization failed", { error });
|
|
10054
13208
|
return err(
|
|
10055
13209
|
HaiAIError.CONFIGURATION_ERROR,
|
|
10056
13210
|
aiM("ai_initFailed", {
|
|
@@ -10065,6 +13219,9 @@ var ai = {
|
|
|
10065
13219
|
get llm() {
|
|
10066
13220
|
return currentLLM ?? notInitializedLLM;
|
|
10067
13221
|
},
|
|
13222
|
+
get acp() {
|
|
13223
|
+
return currentACP ?? notInitializedACP;
|
|
13224
|
+
},
|
|
10068
13225
|
get mcp() {
|
|
10069
13226
|
return currentMCP ?? notInitializedMCP;
|
|
10070
13227
|
},
|
|
@@ -10130,12 +13287,12 @@ var ai = {
|
|
|
10130
13287
|
},
|
|
10131
13288
|
async close() {
|
|
10132
13289
|
if (!currentConfig) {
|
|
10133
|
-
|
|
13290
|
+
logger31.info("AI module already closed, skipping");
|
|
10134
13291
|
return;
|
|
10135
13292
|
}
|
|
10136
|
-
|
|
13293
|
+
logger31.info("Closing AI module");
|
|
10137
13294
|
await resetAllState();
|
|
10138
|
-
|
|
13295
|
+
logger31.info("AI module closed");
|
|
10139
13296
|
}
|
|
10140
13297
|
};
|
|
10141
13298
|
function createMcpServer(options) {
|