mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 14:37:44 +02:00
Merge branch 'main' of https://github.com/multica-ai/super-multica
This commit is contained in:
@@ -38,6 +38,7 @@ export default defineConfig({
|
||||
'@mariozechner/pi-agent-core',
|
||||
'@mariozechner/pi-ai',
|
||||
'@mariozechner/pi-coding-agent',
|
||||
'grammy',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -73,15 +73,25 @@ export class AsyncAgent {
|
||||
if (result.error) {
|
||||
console.error(`[AsyncAgent] Agent run error: ${result.error}`);
|
||||
this.channel.send({ id: uuidv7(), content: `[error] ${result.error}` });
|
||||
this.agent.emitError(result.error);
|
||||
// Only emit agent_error for HTTP 401 from the LLM provider so the
|
||||
// UI shows the "Configure" banner. All other errors (400, tool errors,
|
||||
// etc.) should flow back to the agent for self-recovery.
|
||||
if (/\b401\b/.test(result.error)) {
|
||||
this.agent.emitError(result.error);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[AsyncAgent] Agent run exception: ${message}`);
|
||||
this.channel.send({ id: uuidv7(), content: `[error] ${message}` });
|
||||
// Also emit through subscriber mechanism so IPC listeners receive the error
|
||||
this.agent.emitError(message);
|
||||
// Only emit agent_error for HTTP 401 from the LLM provider so the
|
||||
// UI shows the "Configure" banner. All other errors (400, tool errors,
|
||||
// etc.) should flow back to the agent for self-recovery.
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
if (/\b401\b/.test(errMsg)) {
|
||||
this.agent.emitError(message);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.pendingWrites = Math.max(0, this.pendingWrites - 1);
|
||||
|
||||
@@ -112,8 +112,15 @@ export class Agent {
|
||||
this.reasoningMode = options.reasoningMode ?? "stream";
|
||||
this.output = createAgentOutput({ stdout, stderr: this.stderr, reasoningMode: this.reasoningMode });
|
||||
|
||||
// Resolve provider and model from options > env vars > defaults
|
||||
const defaultProvider = options.provider ?? credentialManager.getLlmProvider() ?? "kimi-coding";
|
||||
// Load session metadata early so stored provider/model can inform defaults
|
||||
this.sessionId = options.sessionId ?? uuidv7();
|
||||
const storedMeta = (() => {
|
||||
const tempSession = new SessionManager({ sessionId: this.sessionId });
|
||||
return tempSession.getMeta();
|
||||
})();
|
||||
|
||||
// Resolve provider and model from options > session meta > env vars > defaults
|
||||
const defaultProvider = options.provider ?? storedMeta?.provider ?? credentialManager.getLlmProvider() ?? "kimi-coding";
|
||||
if (options.authProfileId) {
|
||||
const profileProvider = options.authProfileId.includes(":")
|
||||
? options.authProfileId.split(":")[0]!
|
||||
@@ -195,16 +202,7 @@ export class Agent {
|
||||
});
|
||||
}
|
||||
|
||||
this.sessionId = options.sessionId ?? uuidv7();
|
||||
|
||||
// 解析 model(用于获取 context window)
|
||||
const storedMeta = (() => {
|
||||
// 临时创建 session 获取 meta,避免循环依赖
|
||||
const tempSession = new SessionManager({ sessionId: this.sessionId });
|
||||
return tempSession.getMeta();
|
||||
})();
|
||||
|
||||
const effectiveProvider = resolvedModel ? this.resolvedProvider : (options.provider ?? storedMeta?.provider);
|
||||
const effectiveProvider = this.resolvedProvider;
|
||||
const effectiveModel = resolvedModel ?? options.model ?? storedMeta?.model;
|
||||
let model = resolveModel({ ...options, provider: effectiveProvider, model: effectiveModel });
|
||||
|
||||
@@ -319,7 +317,7 @@ export class Agent {
|
||||
}
|
||||
|
||||
this.session.saveMeta({
|
||||
provider: this.agent.state.model?.provider,
|
||||
provider: this.resolvedProvider,
|
||||
model: this.agent.state.model?.id,
|
||||
thinkingLevel: this.agent.state.thinkingLevel,
|
||||
reasoningMode: this.reasoningMode,
|
||||
@@ -889,9 +887,9 @@ export class Agent {
|
||||
// Update internal state
|
||||
this.resolvedProvider = providerId;
|
||||
|
||||
// Update session metadata
|
||||
// Update session metadata (save original providerId, not alias-resolved)
|
||||
this.session.saveMeta({
|
||||
provider: actualProvider,
|
||||
provider: providerId,
|
||||
model: model.id,
|
||||
thinkingLevel: this.agent.state.thinkingLevel,
|
||||
reasoningMode: this.reasoningMode,
|
||||
|
||||
@@ -65,6 +65,18 @@ describe("heartbeat runner", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("bypasses empty-heartbeat-file check for cron-triggered wakes", async () => {
|
||||
const dir = await mkdtemp(path.join(os.tmpdir(), "heartbeat-test-"));
|
||||
try {
|
||||
await writeFile(path.join(dir, "heartbeat.md"), "# keep empty\n", "utf-8");
|
||||
const agent = createStubAgent({ profileDir: dir, replyText: "HEARTBEAT_OK" });
|
||||
const result = await runHeartbeatOnce({ agent: agent as any, reason: "cron:test-job-id" });
|
||||
expect(result.status).toBe("ran");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("runs and returns ran for heartbeat acknowledgements", async () => {
|
||||
const agent = createStubAgent({ replyText: "HEARTBEAT_OK" });
|
||||
const result = await runHeartbeatOnce({ agent: agent as any, reason: "manual" });
|
||||
|
||||
@@ -156,8 +156,8 @@ export async function runHeartbeatOnce(opts: {
|
||||
}
|
||||
|
||||
try {
|
||||
const isExecEvent = opts.reason === "exec-event";
|
||||
if (!isExecEvent && (await isHeartbeatFileEmpty(agent))) {
|
||||
const isForcedWake = opts.reason === "exec-event" || opts.reason?.startsWith("cron:");
|
||||
if (!isForcedWake && (await isHeartbeatFileEmpty(agent))) {
|
||||
emitHeartbeatEvent({
|
||||
status: "skipped",
|
||||
reason: "empty-heartbeat-file",
|
||||
|
||||
Reference in New Issue
Block a user