@itphx/synth-core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/definitions/protected-paths.json +36 -0
- package/definitions/shell-policy.json +36 -0
- package/dist/definitions/protected-paths.json +36 -0
- package/dist/definitions/shell-policy.json +36 -0
- package/dist/engine/agents/definitions/admin.json +3 -1
- package/dist/engine/agents/definitions/analyst.json +3 -1
- package/dist/engine/agents/definitions/assistant.json +2 -2
- package/dist/engine/agents/definitions/auditor.json +4 -1
- package/dist/engine/agents/definitions/developer.json +4 -1
- package/dist/engine/agents/definitions/devops.json +4 -1
- package/dist/engine/agents/definitions/explore.json +3 -3
- package/dist/engine/agents/definitions/general.json +2 -2
- package/dist/engine/agents/definitions/pm.json +5 -1
- package/dist/engine/agents/definitions/qa.json +4 -1
- package/dist/engine/agents/definitions/title.json +2 -2
- package/dist/engine/agents/index.d.ts +4 -0
- package/dist/engine/agents/index.d.ts.map +1 -1
- package/dist/engine/agents/index.js +9 -3
- package/dist/engine/agents/index.js.map +1 -1
- package/dist/engine/agents/mode.d.ts +5 -3
- package/dist/engine/agents/mode.d.ts.map +1 -1
- package/dist/engine/agents/mode.js +6 -6
- package/dist/engine/agents/mode.js.map +1 -1
- package/dist/engine/agents/mode.test.js +9 -3
- package/dist/engine/agents/mode.test.js.map +1 -1
- package/dist/engine/agents/normalize.d.ts +9 -0
- package/dist/engine/agents/normalize.d.ts.map +1 -0
- package/dist/engine/agents/normalize.js +22 -0
- package/dist/engine/agents/normalize.js.map +1 -0
- package/dist/engine/agents/normalize.test.d.ts +2 -0
- package/dist/engine/agents/normalize.test.d.ts.map +1 -0
- package/dist/engine/agents/normalize.test.js +37 -0
- package/dist/engine/agents/normalize.test.js.map +1 -0
- package/dist/engine/agents/registry.d.ts +2 -1
- package/dist/engine/agents/registry.d.ts.map +1 -1
- package/dist/engine/agents/registry.js +7 -6
- package/dist/engine/agents/registry.js.map +1 -1
- package/dist/engine/agents/registry.test.js +24 -0
- package/dist/engine/agents/registry.test.js.map +1 -1
- package/dist/engine/context/config.d.ts +43 -6
- package/dist/engine/context/config.d.ts.map +1 -1
- package/dist/engine/context/config.js +123 -26
- package/dist/engine/context/config.js.map +1 -1
- package/dist/engine/context/config.security.test.d.ts +2 -0
- package/dist/engine/context/config.security.test.d.ts.map +1 -0
- package/dist/engine/context/config.security.test.js +82 -0
- package/dist/engine/context/config.security.test.js.map +1 -0
- package/dist/engine/context/config.test.js +199 -10
- package/dist/engine/context/config.test.js.map +1 -1
- package/dist/engine/context/optimizer.d.ts +20 -1
- package/dist/engine/context/optimizer.d.ts.map +1 -1
- package/dist/engine/context/optimizer.js +63 -29
- package/dist/engine/context/optimizer.js.map +1 -1
- package/dist/engine/context/optimizer.test.js +156 -0
- package/dist/engine/context/optimizer.test.js.map +1 -1
- package/dist/engine/gateway/gateway-provider.d.ts +4 -0
- package/dist/engine/gateway/gateway-provider.d.ts.map +1 -1
- package/dist/engine/gateway/gateway-provider.js +7 -2
- package/dist/engine/gateway/gateway-provider.js.map +1 -1
- package/dist/engine/gateway/gateway-provider.test.js +3 -3
- package/dist/engine/gateway/gateway-provider.test.js.map +1 -1
- package/dist/engine/gateway/input-guard.d.ts.map +1 -1
- package/dist/engine/gateway/input-guard.js +2 -1
- package/dist/engine/gateway/input-guard.js.map +1 -1
- package/dist/engine/gateway/secret-detector.d.ts.map +1 -1
- package/dist/engine/gateway/secret-detector.js +2 -3
- package/dist/engine/gateway/secret-detector.js.map +1 -1
- package/dist/engine/gateway/secret-detector.test.js +4 -4
- package/dist/engine/gateway/secret-detector.test.js.map +1 -1
- package/dist/engine/llm-error.d.ts +23 -0
- package/dist/engine/llm-error.d.ts.map +1 -0
- package/dist/engine/llm-error.js +86 -0
- package/dist/engine/llm-error.js.map +1 -0
- package/dist/engine/llm-error.test.d.ts +2 -0
- package/dist/engine/llm-error.test.d.ts.map +1 -0
- package/dist/engine/llm-error.test.js +101 -0
- package/dist/engine/llm-error.test.js.map +1 -0
- package/dist/engine/orchestrator-deny-abort.test.d.ts +2 -0
- package/dist/engine/orchestrator-deny-abort.test.d.ts.map +1 -0
- package/dist/engine/orchestrator-deny-abort.test.js +414 -0
- package/dist/engine/orchestrator-deny-abort.test.js.map +1 -0
- package/dist/engine/orchestrator-error.test.d.ts +2 -0
- package/dist/engine/orchestrator-error.test.d.ts.map +1 -0
- package/dist/engine/orchestrator-error.test.js +136 -0
- package/dist/engine/orchestrator-error.test.js.map +1 -0
- package/dist/engine/orchestrator.d.ts +123 -10
- package/dist/engine/orchestrator.d.ts.map +1 -1
- package/dist/engine/orchestrator.js +791 -181
- package/dist/engine/orchestrator.js.map +1 -1
- package/dist/engine/orchestrator.test.js +723 -0
- package/dist/engine/orchestrator.test.js.map +1 -1
- package/dist/engine/permission/approval.d.ts.map +1 -1
- package/dist/engine/permission/approval.js +29 -10
- package/dist/engine/permission/approval.js.map +1 -1
- package/dist/engine/permission/approval.test.js +57 -0
- package/dist/engine/permission/approval.test.js.map +1 -1
- package/dist/engine/permission/evaluator.d.ts +13 -0
- package/dist/engine/permission/evaluator.d.ts.map +1 -1
- package/dist/engine/permission/evaluator.js +121 -22
- package/dist/engine/permission/evaluator.js.map +1 -1
- package/dist/engine/permission/evaluator.test.js +188 -4
- package/dist/engine/permission/evaluator.test.js.map +1 -1
- package/dist/engine/permission/groups.d.ts.map +1 -1
- package/dist/engine/permission/groups.js +19 -1
- package/dist/engine/permission/groups.js.map +1 -1
- package/dist/engine/permission/integration.test.js +165 -4
- package/dist/engine/permission/integration.test.js.map +1 -1
- package/dist/engine/plan.integration.test.js +142 -33
- package/dist/engine/plan.integration.test.js.map +1 -1
- package/dist/engine/router/micro/tasks/classify-intent.d.ts.map +1 -1
- package/dist/engine/router/micro/tasks/classify-intent.js +24 -18
- package/dist/engine/router/micro/tasks/classify-intent.js.map +1 -1
- package/dist/engine/router/micro/tasks/classify-intent.test.js +4 -4
- package/dist/engine/router/micro/tasks/classify-intent.test.js.map +1 -1
- package/dist/engine/router/micro/tasks/enum-decision.d.ts.map +1 -1
- package/dist/engine/router/micro/tasks/enum-decision.js +14 -8
- package/dist/engine/router/micro/tasks/enum-decision.js.map +1 -1
- package/dist/engine/router/micro/tasks/enum-decision.test.js +2 -2
- package/dist/engine/router/micro/tasks/enum-decision.test.js.map +1 -1
- package/dist/engine/router/micro/tasks/small-talk.d.ts.map +1 -1
- package/dist/engine/router/micro/tasks/small-talk.js +20 -17
- package/dist/engine/router/micro/tasks/small-talk.js.map +1 -1
- package/dist/engine/router/micro/types.d.ts +2 -0
- package/dist/engine/router/micro/types.d.ts.map +1 -1
- package/dist/engine/router/provider-pool.d.ts +2 -2
- package/dist/engine/router/provider-pool.d.ts.map +1 -1
- package/dist/engine/router/provider-pool.js +5 -3
- package/dist/engine/router/provider-pool.js.map +1 -1
- package/dist/engine/router/quality-checker.d.ts.map +1 -1
- package/dist/engine/router/quality-checker.js +15 -14
- package/dist/engine/router/quality-checker.js.map +1 -1
- package/dist/engine/router/router.d.ts +2 -2
- package/dist/engine/router/router.d.ts.map +1 -1
- package/dist/engine/router/router.js +8 -5
- package/dist/engine/router/router.js.map +1 -1
- package/dist/engine/router/utils.d.ts +2 -2
- package/dist/engine/router/utils.d.ts.map +1 -1
- package/dist/engine/router/utils.js +2 -1
- package/dist/engine/router/utils.js.map +1 -1
- package/dist/engine/security/index.d.ts +6 -0
- package/dist/engine/security/index.d.ts.map +1 -1
- package/dist/engine/security/index.js +3 -0
- package/dist/engine/security/index.js.map +1 -1
- package/dist/engine/security/integration.test.js +11 -11
- package/dist/engine/security/integration.test.js.map +1 -1
- package/dist/engine/security/prompt-injection.d.ts +1 -1
- package/dist/engine/security/prompt-injection.d.ts.map +1 -1
- package/dist/engine/security/prompt-injection.js +2 -1
- package/dist/engine/security/prompt-injection.js.map +1 -1
- package/dist/engine/security/prompt-injection.test.js +1 -1
- package/dist/engine/security/prompt-injection.test.js.map +1 -1
- package/dist/engine/security/protected-paths-store.d.ts +56 -0
- package/dist/engine/security/protected-paths-store.d.ts.map +1 -0
- package/dist/engine/security/protected-paths-store.js +239 -0
- package/dist/engine/security/protected-paths-store.js.map +1 -0
- package/dist/engine/security/protected-paths.d.ts +54 -0
- package/dist/engine/security/protected-paths.d.ts.map +1 -0
- package/dist/engine/security/protected-paths.js +193 -0
- package/dist/engine/security/protected-paths.js.map +1 -0
- package/dist/engine/security/protected-paths.test.d.ts +2 -0
- package/dist/engine/security/protected-paths.test.d.ts.map +1 -0
- package/dist/engine/security/protected-paths.test.js +166 -0
- package/dist/engine/security/protected-paths.test.js.map +1 -0
- package/dist/engine/security/shell-guard.d.ts +117 -0
- package/dist/engine/security/shell-guard.d.ts.map +1 -0
- package/dist/engine/security/shell-guard.js +578 -0
- package/dist/engine/security/shell-guard.js.map +1 -0
- package/dist/engine/security/shell-guard.test.d.ts +2 -0
- package/dist/engine/security/shell-guard.test.d.ts.map +1 -0
- package/dist/engine/security/shell-guard.test.js +378 -0
- package/dist/engine/security/shell-guard.test.js.map +1 -0
- package/dist/engine/system-messages.d.ts +237 -0
- package/dist/engine/system-messages.d.ts.map +1 -0
- package/dist/engine/system-messages.js +276 -0
- package/dist/engine/system-messages.js.map +1 -0
- package/dist/engine/system-messages.test.d.ts +2 -0
- package/dist/engine/system-messages.test.d.ts.map +1 -0
- package/dist/engine/system-messages.test.js +81 -0
- package/dist/engine/system-messages.test.js.map +1 -0
- package/dist/engine/tools/abort.d.ts +21 -0
- package/dist/engine/tools/abort.d.ts.map +1 -0
- package/dist/engine/tools/abort.js +44 -0
- package/dist/engine/tools/abort.js.map +1 -0
- package/dist/engine/tools/classify-conversation.d.ts.map +1 -1
- package/dist/engine/tools/classify-conversation.js +7 -2
- package/dist/engine/tools/classify-conversation.js.map +1 -1
- package/dist/engine/tools/fs-read.d.ts.map +1 -1
- package/dist/engine/tools/fs-read.js +17 -6
- package/dist/engine/tools/fs-read.js.map +1 -1
- package/dist/engine/tools/fs-read.test.js +43 -0
- package/dist/engine/tools/fs-read.test.js.map +1 -1
- package/dist/engine/tools/fs-write.d.ts.map +1 -1
- package/dist/engine/tools/fs-write.js +47 -17
- package/dist/engine/tools/fs-write.js.map +1 -1
- package/dist/engine/tools/git.d.ts +2 -1
- package/dist/engine/tools/git.d.ts.map +1 -1
- package/dist/engine/tools/git.js +85 -17
- package/dist/engine/tools/git.js.map +1 -1
- package/dist/engine/tools/git.test.js +39 -0
- package/dist/engine/tools/git.test.js.map +1 -1
- package/dist/engine/tools/index.d.ts.map +1 -1
- package/dist/engine/tools/index.js +16 -2
- package/dist/engine/tools/index.js.map +1 -1
- package/dist/engine/tools/json-schema.js +38 -0
- package/dist/engine/tools/json-schema.js.map +1 -1
- package/dist/engine/tools/json-schema.test.d.ts +2 -0
- package/dist/engine/tools/json-schema.test.d.ts.map +1 -0
- package/dist/engine/tools/json-schema.test.js +40 -0
- package/dist/engine/tools/json-schema.test.js.map +1 -0
- package/dist/engine/tools/path-utils.d.ts.map +1 -1
- package/dist/engine/tools/path-utils.js +24 -0
- package/dist/engine/tools/path-utils.js.map +1 -1
- package/dist/engine/tools/path-utils.test.js +15 -0
- package/dist/engine/tools/path-utils.test.js.map +1 -1
- package/dist/engine/tools/plan.d.ts +11 -3
- package/dist/engine/tools/plan.d.ts.map +1 -1
- package/dist/engine/tools/plan.js +326 -95
- package/dist/engine/tools/plan.js.map +1 -1
- package/dist/engine/tools/plan.test.js +161 -47
- package/dist/engine/tools/plan.test.js.map +1 -1
- package/dist/engine/tools/project-context.d.ts.map +1 -1
- package/dist/engine/tools/project-context.js +54 -42
- package/dist/engine/tools/project-context.js.map +1 -1
- package/dist/engine/tools/project-context.test.js +37 -22
- package/dist/engine/tools/project-context.test.js.map +1 -1
- package/dist/engine/tools/project-manage.d.ts +24 -0
- package/dist/engine/tools/project-manage.d.ts.map +1 -0
- package/dist/engine/tools/project-manage.js +311 -0
- package/dist/engine/tools/project-manage.js.map +1 -0
- package/dist/engine/tools/project-manage.test.d.ts +2 -0
- package/dist/engine/tools/project-manage.test.d.ts.map +1 -0
- package/dist/engine/tools/project-manage.test.js +196 -0
- package/dist/engine/tools/project-manage.test.js.map +1 -0
- package/dist/engine/tools/read-reference.d.ts.map +1 -1
- package/dist/engine/tools/read-reference.js +10 -5
- package/dist/engine/tools/read-reference.js.map +1 -1
- package/dist/engine/tools/read-reference.test.js +5 -2
- package/dist/engine/tools/read-reference.test.js.map +1 -1
- package/dist/engine/tools/search.d.ts.map +1 -1
- package/dist/engine/tools/search.js +16 -6
- package/dist/engine/tools/search.js.map +1 -1
- package/dist/engine/tools/shell.d.ts.map +1 -1
- package/dist/engine/tools/shell.js +17 -4
- package/dist/engine/tools/shell.js.map +1 -1
- package/dist/engine/tools/shell.test.js +63 -1
- package/dist/engine/tools/shell.test.js.map +1 -1
- package/dist/engine/tools/task-resume.d.ts +93 -0
- package/dist/engine/tools/task-resume.d.ts.map +1 -0
- package/dist/engine/tools/task-resume.js +269 -0
- package/dist/engine/tools/task-resume.js.map +1 -0
- package/dist/engine/tools/task-resume.test.d.ts +2 -0
- package/dist/engine/tools/task-resume.test.d.ts.map +1 -0
- package/dist/engine/tools/task-resume.test.js +423 -0
- package/dist/engine/tools/task-resume.test.js.map +1 -0
- package/dist/engine/tools/task.d.ts +81 -2
- package/dist/engine/tools/task.d.ts.map +1 -1
- package/dist/engine/tools/task.js +898 -471
- package/dist/engine/tools/task.js.map +1 -1
- package/dist/engine/tools/task.test.js +371 -3
- package/dist/engine/tools/task.test.js.map +1 -1
- package/dist/engine/tools/todo.d.ts +27 -1
- package/dist/engine/tools/todo.d.ts.map +1 -1
- package/dist/engine/tools/todo.js +320 -14
- package/dist/engine/tools/todo.js.map +1 -1
- package/dist/engine/tools/todo.test.js +150 -48
- package/dist/engine/tools/todo.test.js.map +1 -1
- package/dist/engine/tools/web.d.ts.map +1 -1
- package/dist/engine/tools/web.js +65 -18
- package/dist/engine/tools/web.js.map +1 -1
- package/dist/engine/tools/web.test.js +94 -2
- package/dist/engine/tools/web.test.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +0 -1
- package/dist/logger.js.map +1 -1
- package/dist/main.js +3 -2
- package/dist/main.js.map +1 -1
- package/dist/prompts/roles/ADMIN.en.md +12 -11
- package/dist/prompts/roles/ADMIN.ru.md +12 -11
- package/dist/prompts/roles/ANALYST.en.md +12 -11
- package/dist/prompts/roles/ANALYST.ru.md +12 -11
- package/dist/prompts/roles/ASSISTANT.en.md +12 -11
- package/dist/prompts/roles/ASSISTANT.ru.md +12 -11
- package/dist/prompts/roles/AUDITOR.en.md +12 -11
- package/dist/prompts/roles/AUDITOR.ru.md +12 -11
- package/dist/prompts/roles/DEVELOPER.en.md +12 -11
- package/dist/prompts/roles/DEVELOPER.ru.md +12 -11
- package/dist/prompts/roles/DEVOPS.en.md +12 -11
- package/dist/prompts/roles/DEVOPS.ru.md +12 -11
- package/dist/prompts/roles/PM.en.md +12 -11
- package/dist/prompts/roles/PM.ru.md +12 -11
- package/dist/prompts/roles/QA.en.md +12 -11
- package/dist/prompts/roles/QA.ru.md +12 -11
- package/dist/prompts/roles/TITLE.en.md +30 -0
- package/dist/prompts/roles/TITLE.ru.md +3 -11
- package/dist/prompts/security/ask-protected-path.en.md +1 -0
- package/dist/prompts/security/ask-protected-path.ru.md +1 -0
- package/dist/prompts/security/deny-command.en.md +1 -0
- package/dist/prompts/security/deny-command.ru.md +1 -0
- package/dist/prompts/security/deny-protected-dir.en.md +1 -0
- package/dist/prompts/security/deny-protected-dir.ru.md +1 -0
- package/dist/prompts/security/deny-protected-path.en.md +1 -0
- package/dist/prompts/security/deny-protected-path.ru.md +1 -0
- package/dist/prompts/subagents/explore.en.md +23 -0
- package/dist/prompts/subagents/general.en.md +19 -0
- package/dist/prompts/system/MODE_ACT.en.md +20 -0
- package/{prompts/modes/act.md → dist/prompts/system/MODE_ACT.ru.md} +2 -0
- package/dist/prompts/system/MODE_PLAN.en.md +22 -0
- package/dist/prompts/{modes/plan.md → system/MODE_PLAN.ru.md} +4 -3
- package/dist/prompts/system/MODE_YOLO.en.md +22 -0
- package/{prompts/modes/yolo.md → dist/prompts/system/MODE_YOLO.ru.md} +2 -0
- package/dist/prompts/system/PLAN_GUIDE.en.md +36 -0
- package/dist/prompts/system/PLAN_GUIDE.ru.md +33 -0
- package/dist/prompts/system/PLAN_MODE_AUTO.en.md +11 -0
- package/dist/prompts/system/PLAN_MODE_AUTO.ru.md +12 -0
- package/dist/prompts/system/PLAN_MODE_STRICT_APPROVED.en.md +8 -0
- package/dist/prompts/system/PLAN_MODE_STRICT_APPROVED.ru.md +8 -0
- package/dist/prompts/system/PLAN_MODE_STRICT_NOT_APPROVED.en.md +7 -0
- package/dist/prompts/system/PLAN_MODE_STRICT_NOT_APPROVED.ru.md +7 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +28 -2
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/base.d.ts +20 -1
- package/dist/providers/base.d.ts.map +1 -1
- package/dist/providers/base.js +17 -0
- package/dist/providers/base.js.map +1 -1
- package/dist/providers/exceptions.d.ts.map +1 -1
- package/dist/providers/exceptions.js +3 -2
- package/dist/providers/exceptions.js.map +1 -1
- package/dist/providers/ollama.d.ts +23 -0
- package/dist/providers/ollama.d.ts.map +1 -1
- package/dist/providers/ollama.js +128 -12
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/ollama.test.d.ts +2 -0
- package/dist/providers/ollama.test.d.ts.map +1 -0
- package/dist/providers/ollama.test.js +129 -0
- package/dist/providers/ollama.test.js.map +1 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +82 -9
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openai.test.d.ts +2 -0
- package/dist/providers/openai.test.d.ts.map +1 -0
- package/dist/providers/openai.test.js +84 -0
- package/dist/providers/openai.test.js.map +1 -0
- package/dist/providers/stream-error.test.d.ts +2 -0
- package/dist/providers/stream-error.test.d.ts.map +1 -0
- package/dist/providers/stream-error.test.js +63 -0
- package/dist/providers/stream-error.test.js.map +1 -0
- package/dist/providers/timeout.test.d.ts +2 -0
- package/dist/providers/timeout.test.d.ts.map +1 -0
- package/dist/providers/timeout.test.js +129 -0
- package/dist/providers/timeout.test.js.map +1 -0
- package/dist/queue/processor.d.ts +22 -0
- package/dist/queue/processor.d.ts.map +1 -1
- package/dist/queue/processor.js +150 -25
- package/dist/queue/processor.js.map +1 -1
- package/dist/queue/processor.test.d.ts +2 -0
- package/dist/queue/processor.test.d.ts.map +1 -0
- package/dist/queue/processor.test.js +211 -0
- package/dist/queue/processor.test.js.map +1 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +30 -8
- package/dist/server/app.js.map +1 -1
- package/dist/server/chat.d.ts.map +1 -1
- package/dist/server/chat.js +18 -2
- package/dist/server/chat.js.map +1 -1
- package/dist/server/routes/agents.d.ts.map +1 -1
- package/dist/server/routes/agents.js +39 -23
- package/dist/server/routes/agents.js.map +1 -1
- package/dist/server/routes/fs.d.ts.map +1 -1
- package/dist/server/routes/fs.js +185 -6
- package/dist/server/routes/fs.js.map +1 -1
- package/dist/server/routes/models.d.ts.map +1 -1
- package/dist/server/routes/models.js +3 -1
- package/dist/server/routes/models.js.map +1 -1
- package/dist/server/routes/queue.d.ts.map +1 -1
- package/dist/server/routes/queue.js +91 -17
- package/dist/server/routes/queue.js.map +1 -1
- package/dist/server/routes/sessions.d.ts +2 -1
- package/dist/server/routes/sessions.d.ts.map +1 -1
- package/dist/server/routes/sessions.js +68 -10
- package/dist/server/routes/sessions.js.map +1 -1
- package/dist/server/routes/sessions.test.js +102 -2
- package/dist/server/routes/sessions.test.js.map +1 -1
- package/dist/server/routes/settings.d.ts +6 -1
- package/dist/server/routes/settings.d.ts.map +1 -1
- package/dist/server/routes/settings.js +138 -0
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/settings.test.js +65 -1
- package/dist/server/routes/settings.test.js.map +1 -1
- package/dist/server/serialize.d.ts +9 -3
- package/dist/server/serialize.d.ts.map +1 -1
- package/dist/server/serialize.js +9 -3
- package/dist/server/serialize.js.map +1 -1
- package/dist/sessions/auto-title.d.ts +1 -1
- package/dist/sessions/auto-title.d.ts.map +1 -1
- package/dist/sessions/auto-title.js +24 -13
- package/dist/sessions/auto-title.js.map +1 -1
- package/dist/sessions/manager.d.ts +1 -2
- package/dist/sessions/manager.d.ts.map +1 -1
- package/dist/sessions/manager.js +11 -36
- package/dist/sessions/manager.js.map +1 -1
- package/dist/sessions/manager.test.js +2 -10
- package/dist/sessions/manager.test.js.map +1 -1
- package/dist/storage/file-store.d.ts.map +1 -1
- package/dist/storage/file-store.js +2 -6
- package/dist/storage/file-store.js.map +1 -1
- package/dist/storage/file-store.test.js +1 -6
- package/dist/storage/file-store.test.js.map +1 -1
- package/dist/storage/project-store.d.ts.map +1 -1
- package/dist/storage/project-store.js +12 -0
- package/dist/storage/project-store.js.map +1 -1
- package/dist/storage/runtime-settings.d.ts +25 -1
- package/dist/storage/runtime-settings.d.ts.map +1 -1
- package/dist/storage/runtime-settings.js +31 -0
- package/dist/storage/runtime-settings.js.map +1 -1
- package/package.json +6 -5
- package/prompts/roles/TITLE.en.md +30 -0
- package/prompts/security/ask-protected-path.en.md +1 -0
- package/prompts/security/ask-protected-path.ru.md +1 -0
- package/prompts/security/deny-command.en.md +1 -0
- package/prompts/security/deny-command.ru.md +1 -0
- package/prompts/security/deny-protected-dir.en.md +1 -0
- package/prompts/security/deny-protected-dir.ru.md +1 -0
- package/prompts/security/deny-protected-path.en.md +1 -0
- package/prompts/security/deny-protected-path.ru.md +1 -0
- package/prompts/subagents/explore.en.md +23 -0
- package/prompts/subagents/general.en.md +19 -0
- package/prompts/system/MODE_ACT.en.md +20 -0
- package/{dist/prompts/modes/act.md → prompts/system/MODE_ACT.ru.md} +2 -0
- package/prompts/system/MODE_PLAN.en.md +22 -0
- package/{dist/prompts/prompts/modes/plan.md → prompts/system/MODE_PLAN.ru.md} +4 -3
- package/prompts/system/MODE_YOLO.en.md +22 -0
- package/{dist/prompts/prompts/modes/yolo.md → prompts/system/MODE_YOLO.ru.md} +2 -0
- package/prompts/system/PLAN_GUIDE.en.md +36 -0
- package/prompts/system/PLAN_GUIDE.ru.md +33 -0
- package/prompts/system/PLAN_MODE_AUTO.en.md +11 -0
- package/prompts/system/PLAN_MODE_AUTO.ru.md +12 -0
- package/prompts/system/PLAN_MODE_STRICT_APPROVED.en.md +8 -0
- package/prompts/system/PLAN_MODE_STRICT_APPROVED.ru.md +8 -0
- package/prompts/system/PLAN_MODE_STRICT_NOT_APPROVED.en.md +7 -0
- package/prompts/system/PLAN_MODE_STRICT_NOT_APPROVED.ru.md +7 -0
- package/dist/dictionaries/dictionaries/coding.en.yaml +0 -20
- package/dist/dictionaries/dictionaries/coding.ru.yaml +0 -22
- package/dist/dictionaries/dictionaries/coding.tools.yaml +0 -12
- package/dist/dictionaries/dictionaries/enum.en.yaml +0 -54
- package/dist/dictionaries/dictionaries/enum.ru.yaml +0 -67
- package/dist/dictionaries/dictionaries/injection.en.yaml +0 -51
- package/dist/dictionaries/dictionaries/injection.ru.yaml +0 -51
- package/dist/dictionaries/dictionaries/intents.en.yaml +0 -68
- package/dist/dictionaries/dictionaries/intents.ru.yaml +0 -78
- package/dist/dictionaries/dictionaries/research.en.yaml +0 -17
- package/dist/dictionaries/dictionaries/research.ru.yaml +0 -17
- package/dist/dictionaries/dictionaries/small-talk.en.yaml +0 -67
- package/dist/dictionaries/dictionaries/small-talk.ru.yaml +0 -81
- package/dist/prompts/modes/yolo.md +0 -20
- package/dist/prompts/prompts/modes/act.md +0 -18
- package/dist/prompts/prompts/roles/ADMIN.en.md +0 -33
- package/dist/prompts/prompts/roles/ADMIN.ru.md +0 -33
- package/dist/prompts/prompts/roles/ANALYST.en.md +0 -33
- package/dist/prompts/prompts/roles/ANALYST.ru.md +0 -33
- package/dist/prompts/prompts/roles/ASSISTANT.en.md +0 -38
- package/dist/prompts/prompts/roles/ASSISTANT.ru.md +0 -38
- package/dist/prompts/prompts/roles/AUDITOR.en.md +0 -33
- package/dist/prompts/prompts/roles/AUDITOR.ru.md +0 -33
- package/dist/prompts/prompts/roles/DEVELOPER.en.md +0 -42
- package/dist/prompts/prompts/roles/DEVELOPER.ru.md +0 -42
- package/dist/prompts/prompts/roles/DEVOPS.en.md +0 -35
- package/dist/prompts/prompts/roles/DEVOPS.ru.md +0 -35
- package/dist/prompts/prompts/roles/PM.en.md +0 -33
- package/dist/prompts/prompts/roles/PM.ru.md +0 -33
- package/dist/prompts/prompts/roles/QA.en.md +0 -33
- package/dist/prompts/prompts/roles/QA.ru.md +0 -33
- package/dist/prompts/prompts/roles/TITLE.ru.md +0 -30
- package/dist/prompts/prompts/subagents/explore.ru.md +0 -23
- package/dist/prompts/prompts/subagents/general.ru.md +0 -19
- package/dist/prompts/prompts/system/COMPRESSED_DIALOG.en.md +0 -5
- package/dist/prompts/prompts/system/COMPRESSED_DIALOG.ru.md +0 -5
- package/dist/prompts/prompts/system/INTERVIEW.en.md +0 -18
- package/dist/prompts/prompts/system/INTERVIEW.ru.md +0 -18
- package/dist/prompts/prompts/system/PARTICIPANTS_NOTICE.en.md +0 -7
- package/dist/prompts/prompts/system/PARTICIPANTS_NOTICE.ru.md +0 -7
- package/dist/prompts/prompts/system/ROLES_DESCRIPTION.en.md +0 -16
- package/dist/prompts/prompts/system/ROLES_DESCRIPTION.ru.md +0 -16
- package/dist/prompts/prompts/system/ROUTING.en.md +0 -13
- package/dist/prompts/prompts/system/ROUTING.md +0 -6
- package/dist/prompts/prompts/system/ROUTING.ru.md +0 -13
- package/dist/prompts/prompts/system/RULES.en.md +0 -10
- package/dist/prompts/prompts/system/RULES.md +0 -10
- package/dist/prompts/prompts/system/RULES.ru.md +0 -10
- package/dist/prompts/prompts/system/STICKY_NOTES.en.md +0 -6
- package/dist/prompts/prompts/system/STICKY_NOTES.ru.md +0 -6
- package/dist/prompts/prompts/system/SUMMARIZER.en.md +0 -54
- package/dist/prompts/prompts/system/SUMMARIZER.ru.md +0 -54
- package/dist/prompts/prompts/system/USER_CONTEXT.en.md +0 -5
- package/dist/prompts/prompts/system/USER_CONTEXT.ru.md +0 -5
- package/dist/prompts/system/ROUTING.md +0 -6
- package/dist/prompts/system/RULES.md +0 -10
- package/dist/server/routes/i18n.d.ts +0 -3
- package/dist/server/routes/i18n.d.ts.map +0 -1
- package/dist/server/routes/i18n.js +0 -83
- package/dist/server/routes/i18n.js.map +0 -1
- package/prompts/modes/plan.md +0 -20
- package/prompts/system/ROUTING.md +0 -6
- package/prompts/system/RULES.md +0 -10
- /package/dist/prompts/{prompts/system → system}/GATEWAY.en.md +0 -0
- /package/dist/prompts/{prompts/system → system}/GATEWAY.ru.md +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { MessageRole, nowISO, ToolCapability, generateId, makeTextPart } from '@itphx/synth-shared';
|
|
1
|
+
import { MessageRole, nowISO, ToolCapability, generateId, makeTextPart, lookupContextWindow, generateStreamId } from '@itphx/synth-shared';
|
|
2
2
|
import { join, isAbsolute, normalize, resolve } from 'node:path';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { DebugCollector } from '../debug.js';
|
|
5
5
|
import { ProviderFactory } from '../providers/base.js';
|
|
6
|
+
import { ContextLengthExceededError } from '../providers/exceptions.js';
|
|
7
|
+
import { renderPlanMarkdown } from './tools/plan.js';
|
|
8
|
+
import { renderTodosMarkdown } from './tools/todo.js';
|
|
6
9
|
import { info, warning } from '../logger.js';
|
|
7
10
|
import { autoTitleSession } from '../sessions/auto-title.js';
|
|
8
11
|
import { ModelRouter } from './router/router.js';
|
|
@@ -13,15 +16,21 @@ import { DictionaryStore } from './router/dictionary-store.js';
|
|
|
13
16
|
import { MicroPipeline } from './router/micro/index.js';
|
|
14
17
|
import { TaskClassifier } from './router/micro/task-classifier.js';
|
|
15
18
|
import { RulesModel, EmbeddingModel, MicroLlmModel } from './router/micro/models/index.js';
|
|
16
|
-
import { PermissionEvaluator } from './permission/evaluator.js';
|
|
19
|
+
import { PermissionEvaluator, isPlanRestricted, isPlanBlockedTool, getMcpToolAvailability, extractPaths } from './permission/evaluator.js';
|
|
17
20
|
import { PermissionApprovalManager } from './permission/approval.js';
|
|
18
21
|
import { emptyPermissionConfig } from './permission/derive.js';
|
|
22
|
+
import { classifyLlmError, isLlmErrorRecoverable } from './llm-error.js';
|
|
23
|
+
import { runSubagentResume, validateSubagentResume, SubagentResumeError } from './tools/task-resume.js';
|
|
19
24
|
import { getToolTrust } from './security/trust.js';
|
|
20
25
|
import { detectPromptInjection, applyPolicy, wrapContent, buildWarnLine } from './security/prompt-injection.js';
|
|
26
|
+
import { loadProtectedPaths } from './security/protected-paths.js';
|
|
21
27
|
import { GatewayProvider, AuditLogger, RateLimiter, ObfuscationMapStore, obfuscateSecrets, maskSecret, PLACEHOLDER_PATTERN } from './gateway/index.js';
|
|
28
|
+
import { t as sm, i18n as i18nMeta } from './system-messages.js';
|
|
22
29
|
const MAX_DOOM_LOOP_COUNT = 3;
|
|
23
30
|
const DEFAULT_MAX_ITERATIONS = Number.POSITIVE_INFINITY;
|
|
24
|
-
|
|
31
|
+
/** Порог заполненности контекстного окна (%), после которого публикуем context.limit_warning. */
|
|
32
|
+
const CONTEXT_LIMIT_WARNING_PERCENT = 90;
|
|
33
|
+
function toCoreProviderConfig(shared, model, reasoningLevel) {
|
|
25
34
|
return {
|
|
26
35
|
name: shared.name,
|
|
27
36
|
type: shared.type,
|
|
@@ -29,6 +38,7 @@ function toCoreProviderConfig(shared, model) {
|
|
|
29
38
|
apiKey: shared.apiKey || '',
|
|
30
39
|
model,
|
|
31
40
|
timeout: shared.timeout,
|
|
41
|
+
reasoningLevel,
|
|
32
42
|
};
|
|
33
43
|
}
|
|
34
44
|
/**
|
|
@@ -70,10 +80,44 @@ export class ChatOrchestrator {
|
|
|
70
80
|
/** Внешний audit-logger (backend/runner) — переиспользуется при live-включении gateway. */
|
|
71
81
|
_gatewayAuditExternal;
|
|
72
82
|
_providerPool;
|
|
83
|
+
/** Политика защищённых путей (СКВОЗНАЯ проверка файловых тулов + shell) — ленивая загрузка с кэшем. */
|
|
84
|
+
_protectedPaths = null;
|
|
85
|
+
_protectedPathsLoaded = false;
|
|
73
86
|
/** Публичный доступ к реестру агентов (для сериализации modeApplicable). */
|
|
74
87
|
get agentRegistry() {
|
|
75
88
|
return this._agentRegistry;
|
|
76
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Политика защищённых путей: системный дефолт definitions/protected-paths.json
|
|
92
|
+
* + override {dataDir}/security/protected-paths.json (каскад add/remove).
|
|
93
|
+
* Ленивая загрузка с кэшем; при недоступности dataDir/файлов — системный дефолт.
|
|
94
|
+
*/
|
|
95
|
+
_getProtectedPaths() {
|
|
96
|
+
if (this._protectedPathsLoaded)
|
|
97
|
+
return this._protectedPaths;
|
|
98
|
+
this._protectedPathsLoaded = true;
|
|
99
|
+
try {
|
|
100
|
+
this._protectedPaths = loadProtectedPaths({ overridePath: join(this._dataDir, 'security', 'protected-paths.json') });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
this._protectedPaths = null;
|
|
104
|
+
}
|
|
105
|
+
return this._protectedPaths;
|
|
106
|
+
}
|
|
107
|
+
/** Включена ли защита защищённых путей (тумблер полного отключения). */
|
|
108
|
+
_getProtectedPathsEnabled() {
|
|
109
|
+
return this._securityConfig.protectedPaths?.enabled ?? true;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Локализованный текст сообщения безопасности для model/UI. Для решений с
|
|
113
|
+
* messageId (защищённые пути, запрещённые команды) берётся из промпт-файла;
|
|
114
|
+
* иначе — обычный reason. Текст включает префикс "Permission denied/required".
|
|
115
|
+
*/
|
|
116
|
+
_securityMessage(decision, lang) {
|
|
117
|
+
if (!decision.messageId)
|
|
118
|
+
return undefined;
|
|
119
|
+
return this._contextConfig?.getSecurityMessage(decision.messageId, lang, decision.params) ?? undefined;
|
|
120
|
+
}
|
|
77
121
|
constructor(options) {
|
|
78
122
|
this._sessionManager = options.sessionManager;
|
|
79
123
|
this._eventBus = options.eventBus;
|
|
@@ -93,7 +137,7 @@ export class ChatOrchestrator {
|
|
|
93
137
|
this._approvalManager = new PermissionApprovalManager(this._eventBus);
|
|
94
138
|
this._defaultPlanMode = options.defaultPlanMode ?? 'auto';
|
|
95
139
|
this._securityConfig = options.securityConfig
|
|
96
|
-
?? { promptInjection: { enabled: true, wrapContent: true, mode: 'flag' } };
|
|
140
|
+
?? { promptInjection: { enabled: true, wrapContent: true, mode: 'flag' }, protectedPaths: { enabled: true } };
|
|
97
141
|
this._dataDir = options.dataDir || '/var/synth/data';
|
|
98
142
|
this._gatewayPayloadLogPath = options.gatewayPayloadLogPath
|
|
99
143
|
?? join(options.dataDir || '/var/synth/data', 'audit', 'llm-payloads.jsonl');
|
|
@@ -210,6 +254,22 @@ export class ChatOrchestrator {
|
|
|
210
254
|
isGatewayEnabled() {
|
|
211
255
|
return !!this._gatewayConfig;
|
|
212
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Live-переключение секции «Безопасность» (runtime-настройка из UI).
|
|
259
|
+
* Обновляет тумблер защищённых путей без рестарта сервера.
|
|
260
|
+
*/
|
|
261
|
+
setSecurityConfig(config) {
|
|
262
|
+
this._securityConfig = config
|
|
263
|
+
?? { promptInjection: { enabled: true, wrapContent: true, mode: 'flag' }, protectedPaths: { enabled: true } };
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* AD-029: live-применение глобального таймаута подтверждения (runtime-настройка
|
|
267
|
+
* из UI, раздел «Безопасность»). Обновляет дефолтный permission-конфиг, который
|
|
268
|
+
* наследуют сессии без собственного пер-сессионного permission-конфига.
|
|
269
|
+
*/
|
|
270
|
+
setDefaultPermissionTimeout(askTimeoutMs) {
|
|
271
|
+
this._permissionConfig = { ...this._permissionConfig, askTimeoutMs };
|
|
272
|
+
}
|
|
213
273
|
/**
|
|
214
274
|
* AD-036: обфусцирует user-сообщение при сохранении в историю (до отправки в LLM).
|
|
215
275
|
* Карта обфускации сессии обновляется и сохраняется; в message.metadata.secrets
|
|
@@ -278,49 +338,159 @@ export class ChatOrchestrator {
|
|
|
278
338
|
return dir;
|
|
279
339
|
}
|
|
280
340
|
/**
|
|
281
|
-
* AD-025/AD-
|
|
282
|
-
* утверждённого
|
|
283
|
-
* после plan_enter/plan_exit в текущей итерации.
|
|
341
|
+
* AD-025/AD-039: добавляет в system prompt блоки режима Plan/Act и
|
|
342
|
+
* утверждённого плана проекта. Вызывается при старте обработки и
|
|
343
|
+
* пересобирается после plan_enter/plan_exit в текущей итерации.
|
|
284
344
|
*/
|
|
285
|
-
_buildPlanSystemPrompt(base, session, planMode, sessionMode, planApproved
|
|
345
|
+
async _buildPlanSystemPrompt(base, session, planMode, sessionMode, planApproved) {
|
|
286
346
|
const parts = [];
|
|
287
|
-
// AD-025: промпт текущего режима загружается из
|
|
288
|
-
// (prompts/
|
|
289
|
-
// Если
|
|
347
|
+
// AD-025: промпт текущего режима загружается из contextConfig.getModePrompt
|
|
348
|
+
// (prompts/system/MODE_{PLAN,ACT,YOLO}.{lang}.md, переопределяемые через dataDir/context/user/system).
|
|
349
|
+
// Если getModePrompt вернул null (нет contextConfig или файла) — блок режима
|
|
350
|
+
// не добавляется ни для одного режима.
|
|
290
351
|
const language = session.settings.language || 'ru';
|
|
291
352
|
const modePrompt = this._contextConfig?.getModePrompt(sessionMode, language) ?? null;
|
|
292
353
|
if (modePrompt) {
|
|
293
354
|
parts.push(modePrompt);
|
|
294
355
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
356
|
+
// AD-028/AD-039: промпт строгости цикла Plan→Build загружается из contextConfig
|
|
357
|
+
// (prompts/system/PLAN_MODE_{AUTO,STRICT_APPROVED,STRICT_NOT_APPROVED}.{lang}.md,
|
|
358
|
+
// переопределяемые через dataDir/context/user/system). planMode 'off' → null —
|
|
359
|
+
// блок не добавляется.
|
|
360
|
+
const planModePrompt = this._contextConfig?.getPlanModePrompt(planMode, planApproved, language) ?? null;
|
|
361
|
+
if (planModePrompt) {
|
|
362
|
+
parts.push(planModePrompt);
|
|
363
|
+
}
|
|
364
|
+
// AD-039: блок [APPROVED PLAN] собирается из плана проекта (project.plan).
|
|
365
|
+
const approvedPlan = await this._renderProjectPlan(session);
|
|
366
|
+
if (approvedPlan) {
|
|
367
|
+
parts.push(`[APPROVED PLAN]\n${approvedPlan}`);
|
|
368
|
+
}
|
|
369
|
+
// AD-039: блок [TODO] собирается из session.todos.
|
|
370
|
+
const todosMarkdown = renderTodosMarkdown(session.todos ?? []);
|
|
371
|
+
if (todosMarkdown) {
|
|
372
|
+
parts.push(`[TODO]\n${todosMarkdown}`);
|
|
373
|
+
}
|
|
374
|
+
// AD-039: инструкция по ведению плана и todo (PLAN_GUIDE) встраивается
|
|
375
|
+
// всегда, когда доступен контекст (contextConfig), независимо от наличия
|
|
376
|
+
// плана или todo — модель должна знать разницу между ними с первого запроса.
|
|
377
|
+
const guide = this._contextConfig?.getPlanGuide(language) ?? null;
|
|
378
|
+
if (guide) {
|
|
379
|
+
parts.push(guide);
|
|
380
|
+
}
|
|
381
|
+
if (parts.length === 0)
|
|
382
|
+
return base;
|
|
383
|
+
return `${base}\n\n${parts.join('\n\n')}`;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* AD-025: фильтрует список тулов, передаваемых LLM, по текущему режиму сессии.
|
|
387
|
+
* В plan-режиме (или strict без утверждённого плана) из списка убираются
|
|
388
|
+
* write/edit-тулы и git-запись (группы edit/git_write) — модель не видит
|
|
389
|
+
* инструментов, которые всё равно будут отклонены PermissionEvaluator'ом.
|
|
390
|
+
* MCP-инструменты фильтруются по настройкам доступности (plan/act).
|
|
391
|
+
* Режимы act/yolo — полный список без изменений.
|
|
392
|
+
*/
|
|
393
|
+
_filterToolsByMode(tools, session, sessionMode, planMode, planApproved) {
|
|
394
|
+
if (!tools || tools.length === 0)
|
|
395
|
+
return tools;
|
|
396
|
+
// Рестрикция не активна (act/yolo/off) — полный список тулов.
|
|
397
|
+
if (!isPlanRestricted(sessionMode, planMode, planApproved))
|
|
398
|
+
return tools;
|
|
399
|
+
const mcpToolSettings = session.settings.mcpToolSettings;
|
|
400
|
+
const result = [];
|
|
401
|
+
for (const tool of tools) {
|
|
402
|
+
const toolName = tool.function.name;
|
|
403
|
+
const toolDef = this._toolRegistry?.get(toolName);
|
|
404
|
+
const capabilities = toolDef?.capabilities;
|
|
405
|
+
// Edit-группа (write/edit/delete) и git-запись блокируются в plan-режиме —
|
|
406
|
+
// скрываем их от модели, чтобы она не пыталась их вызывать.
|
|
407
|
+
if (isPlanBlockedTool(toolName, capabilities))
|
|
408
|
+
continue;
|
|
409
|
+
// MCP-инструменты: в plan-режиме недоступный по настройкам тул убираем из
|
|
410
|
+
// списка (консистентно с PermissionEvaluator: modeKey = 'plan').
|
|
411
|
+
if (capabilities?.includes(ToolCapability.MCP) && mcpToolSettings) {
|
|
412
|
+
const availability = getMcpToolAvailability(mcpToolSettings, toolName);
|
|
413
|
+
if (availability && !availability.plan)
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
result.push(tool);
|
|
417
|
+
}
|
|
418
|
+
return result;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* AD-039: markdown-рендер плана проекта. Null — если проекта нет, нет plan
|
|
422
|
+
* или план пуст (нет пунктов).
|
|
423
|
+
*/
|
|
424
|
+
async _renderProjectPlan(session) {
|
|
425
|
+
if (!session.projectId || !this._projectStore)
|
|
426
|
+
return null;
|
|
427
|
+
try {
|
|
428
|
+
const project = await this._projectStore.load(session.projectId);
|
|
429
|
+
const plan = project?.plan;
|
|
430
|
+
if (!plan || plan.items.length === 0)
|
|
431
|
+
return null;
|
|
432
|
+
return renderPlanMarkdown(plan);
|
|
300
433
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
+ 'Если задача сложная, затрагивает несколько файлов или требует архитектурных решений — '
|
|
304
|
-
+ 'вызови plan_enter перед изменениями. Для простых фиксов — делай сразу.');
|
|
434
|
+
catch {
|
|
435
|
+
return null;
|
|
305
436
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* AD-040: добавляет в system prompt контекстный блок сессии — наличие/отсутствие
|
|
440
|
+
* проекта, рабочий каталог и ограничения. Вызывается после резолва workingDir.
|
|
441
|
+
*/
|
|
442
|
+
async _buildSessionContextPrompt(base, session, workingDir) {
|
|
443
|
+
const lang = session.settings.language || 'ru';
|
|
444
|
+
const lines = [];
|
|
445
|
+
if (session.projectId) {
|
|
446
|
+
let project = null;
|
|
447
|
+
if (this._projectStore) {
|
|
448
|
+
try {
|
|
449
|
+
project = await this._projectStore.load(session.projectId);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
project = null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (project) {
|
|
456
|
+
lines.push('[КОНТЕКСТ СЕССИИ]');
|
|
457
|
+
lines.push(`Проект: ${project.name}`);
|
|
458
|
+
lines.push(`ID проекта: ${session.projectId}`);
|
|
459
|
+
lines.push(`Рабочий каталог: ${project.workspace || workingDir}`);
|
|
460
|
+
const invariants = project.invariants?.trim();
|
|
461
|
+
if (invariants) {
|
|
462
|
+
lines.push('');
|
|
463
|
+
lines.push(lang === 'en' ? '[PROJECT INVARIANTS]' : '[ИНВАРИАНТЫ ПРОЕКТА]');
|
|
464
|
+
lines.push(project.invariants);
|
|
465
|
+
}
|
|
310
466
|
}
|
|
311
467
|
else {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
468
|
+
lines.push('[КОНТЕКСТ СЕССИИ]');
|
|
469
|
+
lines.push(`Проект: не найден (id: ${session.projectId})`);
|
|
470
|
+
lines.push(`Рабочий каталог: ${workingDir}`);
|
|
315
471
|
}
|
|
316
472
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
473
|
+
else if (lang === 'en') {
|
|
474
|
+
lines.push('[SESSION CONTEXT]');
|
|
475
|
+
lines.push('Project: none');
|
|
476
|
+
lines.push(`Working directory: ${workingDir}`);
|
|
477
|
+
lines.push('Limitations: a plan is unavailable (the plan is bound to a project). '
|
|
478
|
+
+ 'File and git tools operate within the working directory. Todo is available.');
|
|
479
|
+
lines.push('If the task requires long-term work with files, git, or a plan — suggest that the user '
|
|
480
|
+
+ 'create a project and move the session into it.');
|
|
320
481
|
}
|
|
321
|
-
|
|
482
|
+
else {
|
|
483
|
+
lines.push('[КОНТЕКСТ СЕССИИ]');
|
|
484
|
+
lines.push('Проект: отсутствует');
|
|
485
|
+
lines.push(`Рабочий каталог: ${workingDir}`);
|
|
486
|
+
lines.push('Ограничения: план недоступен (план привязан к проекту). '
|
|
487
|
+
+ 'Файловые и git-инструменты работают в рамках рабочего каталога. Todo доступен.');
|
|
488
|
+
lines.push('Если задача требует длительной работы с файлами, git или планом — предложи пользователю '
|
|
489
|
+
+ 'создать проект и перенести сессию в него.');
|
|
490
|
+
}
|
|
491
|
+
if (lines.length === 0)
|
|
322
492
|
return base;
|
|
323
|
-
return `${base}\n\n${
|
|
493
|
+
return `${base}\n\n${lines.join('\n')}`;
|
|
324
494
|
}
|
|
325
495
|
/**
|
|
326
496
|
* processMessage callback для QueueProcessor.
|
|
@@ -332,13 +502,13 @@ export class ChatOrchestrator {
|
|
|
332
502
|
}
|
|
333
503
|
async _processMessage(session, message, signal, publishChunk) {
|
|
334
504
|
const streamId = message.metadata?.stream_id || 'unknown';
|
|
335
|
-
const effectiveAgentRole = message.agentRole || session.settings.agentRole
|
|
505
|
+
const effectiveAgentRole = message.agentRole || session.settings.agentRole;
|
|
336
506
|
const author = message.author || 'unknown';
|
|
337
507
|
const workingDir = await this._resolveWorkingDir(session);
|
|
338
508
|
// AD-025: режим Plan/Act — runtime-состояние сессии + строгость из настроек
|
|
339
509
|
const planMode = session.settings.planMode ?? this._defaultPlanMode ?? 'auto';
|
|
340
510
|
let sessionMode = session.mode ?? 'act';
|
|
341
|
-
let planApproved = Boolean(session.
|
|
511
|
+
let planApproved = Boolean(session.planApproved);
|
|
342
512
|
// AD-029: permissions — сессионный override приоритетнее глобального конфига.
|
|
343
513
|
// Конфиг может быть расширен «разрешить в сессии» (once:false) в процессе
|
|
344
514
|
// обработки, поэтому ссылка и эвалуатор пересоздаются.
|
|
@@ -349,13 +519,15 @@ export class ChatOrchestrator {
|
|
|
349
519
|
const maxIterations = (agentMaxSteps !== undefined && agentMaxSteps > 0)
|
|
350
520
|
? agentMaxSteps
|
|
351
521
|
: DEFAULT_MAX_ITERATIONS;
|
|
522
|
+
// Уровень размышлений агента: если агент не задал — берём настройку сессии
|
|
523
|
+
const effectiveReasoningLevel = agentDef?.reasoningLevel ?? session.settings.reasoningLevel;
|
|
352
524
|
const debugCollector = new DebugCollector(session.settings.debugEnabled);
|
|
353
525
|
// --- Обработка системных сообщений (суммаризация, шедулер, etc.) ---
|
|
354
526
|
if (message.role === MessageRole.SYSTEM && message.metadata?.type === 'summarization_request') {
|
|
355
527
|
return this._handleSummarization(session, message, signal, publishChunk, debugCollector, streamId);
|
|
356
528
|
}
|
|
357
529
|
if (message.role === MessageRole.SYSTEM && message.metadata?.type === 'debug_system_message') {
|
|
358
|
-
return this._handleDebugSystemMessage(session, message, signal, publishChunk, debugCollector, streamId);
|
|
530
|
+
return this._handleDebugSystemMessage(session, message, signal, publishChunk, debugCollector, streamId, effectiveReasoningLevel);
|
|
359
531
|
}
|
|
360
532
|
// --- AD-035: Micro-first — пробуем ответить без LLM ---
|
|
361
533
|
if (this._microPipeline && session.settings.microFirst) {
|
|
@@ -380,6 +552,9 @@ export class ChatOrchestrator {
|
|
|
380
552
|
// Сохраняем сообщение ассистента
|
|
381
553
|
const microMetadata = {
|
|
382
554
|
stream_id: streamId,
|
|
555
|
+
...(microResult.microResult.i18nKey
|
|
556
|
+
? { i18n: i18nMeta(microResult.microResult.i18nKey) }
|
|
557
|
+
: {}),
|
|
383
558
|
micro_first: {
|
|
384
559
|
label: microResult.microResult.label,
|
|
385
560
|
confidence: microResult.microResult.confidence,
|
|
@@ -413,7 +588,7 @@ export class ChatOrchestrator {
|
|
|
413
588
|
// Авто-тайтл если это первое сообщение
|
|
414
589
|
if (session.messages.filter((m) => m.role === MessageRole.USER).length <= 1) {
|
|
415
590
|
try {
|
|
416
|
-
session.title =
|
|
591
|
+
session.title = sm('session.dialog_title', { id: session.id.slice(0, 8) });
|
|
417
592
|
await this._sessionManager.saveSession(session);
|
|
418
593
|
}
|
|
419
594
|
catch {
|
|
@@ -434,11 +609,11 @@ export class ChatOrchestrator {
|
|
|
434
609
|
}
|
|
435
610
|
try {
|
|
436
611
|
// 1. Get provider (через роутер если доступен, иначе напрямую)
|
|
437
|
-
const routeResult = this._routeProvider(session, message);
|
|
612
|
+
const routeResult = this._routeProvider(session, message, effectiveReasoningLevel);
|
|
438
613
|
if (!routeResult) {
|
|
439
614
|
const providerName = message.provider || session.settings.provider || '';
|
|
440
615
|
return {
|
|
441
|
-
error:
|
|
616
|
+
error: sm('router.provider_not_found', { provider: providerName || sm('router.provider_unspecified') }),
|
|
442
617
|
errorType: 'provider_not_found',
|
|
443
618
|
canRetry: false,
|
|
444
619
|
};
|
|
@@ -487,8 +662,10 @@ export class ChatOrchestrator {
|
|
|
487
662
|
finalSystemPrompt = `${finalSystemPrompt}\n\n${gatewayInstructions}`;
|
|
488
663
|
}
|
|
489
664
|
}
|
|
490
|
-
// AD-025/AD-
|
|
491
|
-
finalSystemPrompt = this._buildPlanSystemPrompt(finalSystemPrompt, session, planMode, sessionMode, planApproved);
|
|
665
|
+
// AD-025/AD-039: блоки режима Plan/Act + утверждённый план проекта
|
|
666
|
+
finalSystemPrompt = await this._buildPlanSystemPrompt(finalSystemPrompt, session, planMode, sessionMode, planApproved);
|
|
667
|
+
// AD-040: контекстный блок сессии (проект/рабочий каталог/ограничения)
|
|
668
|
+
finalSystemPrompt = await this._buildSessionContextPrompt(finalSystemPrompt, session, workingDir);
|
|
492
669
|
// 3. Convert messages for LLM (без summary — они уже в system prompt)
|
|
493
670
|
const messagesForLlm = optimizedMessages.filter((m) => m.role !== MessageRole.SUMMARY);
|
|
494
671
|
const baseLlmMessages = ChatOrchestrator._sessionMessagesToLlm(messagesForLlm, effectiveAgentRole, author);
|
|
@@ -518,32 +695,45 @@ export class ChatOrchestrator {
|
|
|
518
695
|
}
|
|
519
696
|
}
|
|
520
697
|
}
|
|
521
|
-
// 4. Get tools from registry, filtered by agent capabilities
|
|
522
|
-
|
|
698
|
+
// 4. Get tools from registry, filtered by agent capabilities.
|
|
699
|
+
// baseTools — полный набор по агентам; tools — с учётом режима сессии
|
|
700
|
+
// (AD-025): пересобирается из baseTools после plan_enter/plan_exit.
|
|
701
|
+
let baseTools;
|
|
523
702
|
if (this._toolRegistry) {
|
|
524
|
-
|
|
525
|
-
|
|
703
|
+
// agentRole всегда определён при нормальной работе (пользователь, @-mention,
|
|
704
|
+
// сабагент). Если агент НЕ найден в реестре или role undefined — модель получает
|
|
705
|
+
// НИКАКИХ инструментов. Никакого молчаливого fallback на «все non-MCP», иначе
|
|
706
|
+
// агент без shell видит execute_shell и «гадает по именам» вместо честного ответа.
|
|
707
|
+
const agentDef = effectiveAgentRole ? this._agentRegistry?.get(effectiveAgentRole) : undefined;
|
|
708
|
+
if (agentDef && effectiveAgentRole) {
|
|
709
|
+
baseTools = this._agentRegistry.getToolsForAgent(effectiveAgentRole);
|
|
526
710
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const allNonMcp = Object.values(ToolCapability).filter((c) => c !== ToolCapability.MCP);
|
|
530
|
-
tools = this._toolRegistry.filterByCapabilities(allNonMcp);
|
|
531
|
-
}
|
|
532
|
-
else if (tools.length === 0) {
|
|
533
|
-
tools = [];
|
|
711
|
+
else {
|
|
712
|
+
baseTools = [];
|
|
534
713
|
}
|
|
535
714
|
}
|
|
715
|
+
let tools = this._filterToolsByMode(baseTools, session, sessionMode, planMode, planApproved);
|
|
536
716
|
// 5. Create cancellation token
|
|
717
|
+
// Внутренний AbortController: абортится и по cancelToken.cancel()
|
|
718
|
+
// (POST /sessions/:id/abort → orchestrator.cancel), и по внешнему signal
|
|
719
|
+
// (cancelRetry в QueueProcessor). Его signal передаётся в тулы
|
|
720
|
+
// (ctx.abortSignal) — долгие тулы (например task/сабагент) подписываются
|
|
721
|
+
// на него и прерываются сразу, не дожидаясь завершения.
|
|
722
|
+
const toolAbortController = new AbortController();
|
|
537
723
|
const cancelToken = {
|
|
538
724
|
cancelled: false,
|
|
539
725
|
cancel: () => {
|
|
540
726
|
cancelToken.cancelled = true;
|
|
727
|
+
toolAbortController.abort();
|
|
541
728
|
},
|
|
542
729
|
};
|
|
543
730
|
this._cancellationTokens.set(streamId, cancelToken);
|
|
544
731
|
signal.addEventListener('abort', () => {
|
|
545
732
|
cancelToken.cancel();
|
|
546
733
|
}, { once: true });
|
|
734
|
+
// signal мог быть уже абортирован до входа (гонка с отменой из очереди)
|
|
735
|
+
if (signal.aborted)
|
|
736
|
+
cancelToken.cancel();
|
|
547
737
|
// 6. Tool loop — stream LLM, execute tools, continue until no tool calls
|
|
548
738
|
let toolIteration = 0;
|
|
549
739
|
const currentMessages = [...baseLlmMessages];
|
|
@@ -554,8 +744,17 @@ export class ChatOrchestrator {
|
|
|
554
744
|
let finalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
555
745
|
let finalModel;
|
|
556
746
|
const allToolCalls = [];
|
|
747
|
+
const allRawToolCalls = [];
|
|
557
748
|
let stopAfterDeny = false;
|
|
558
749
|
let denyStopReason;
|
|
750
|
+
/** metadata.i18n подставленного финального фоллбека (лимит итераций / deny / пустой ответ). */
|
|
751
|
+
let fallbackI18n;
|
|
752
|
+
/** Были ли deny-отказы за цикл (для осмысленного fallback пустого ответа). */
|
|
753
|
+
let hadDeny = false;
|
|
754
|
+
/** Отмена пришла во время исполнения тула — завершать обработку как отмену. */
|
|
755
|
+
let cancelledDuringTool = false;
|
|
756
|
+
// Предупреждение о заполненности контекстного окна (fire-and-forget, не блокирует вызов)
|
|
757
|
+
this._publishContextLimitWarning(session.id, routeResult.modelName, messagesForLlm);
|
|
559
758
|
while (toolIteration < maxIterations) {
|
|
560
759
|
if (cancelToken.cancelled)
|
|
561
760
|
break;
|
|
@@ -611,23 +810,28 @@ export class ChatOrchestrator {
|
|
|
611
810
|
catch (e) {
|
|
612
811
|
const errMsg = String(e);
|
|
613
812
|
warning('ORCHESTRATOR', `LLM stream error: ${errMsg}`);
|
|
614
|
-
if (accumulatedContent || accumulatedReasoning) {
|
|
615
|
-
await this._saveAssistantMessage(session.id, accumulatedContent, streamId, effectiveAgentRole, finalUsage, chunkModel, accumulatedReasoning, debugCollector, true, providerName);
|
|
616
|
-
}
|
|
617
813
|
this._cancellationTokens.delete(streamId);
|
|
618
|
-
return
|
|
619
|
-
error: errMsg,
|
|
620
|
-
errorType: this._classifyError(errMsg),
|
|
621
|
-
canRetry: this._isRecoverable(errMsg),
|
|
622
|
-
};
|
|
814
|
+
return this._handleLlmFailure(session.id, streamId, e, publishChunk);
|
|
623
815
|
}
|
|
624
816
|
if (cancelToken.cancelled) {
|
|
625
|
-
|
|
626
|
-
|
|
817
|
+
// Отмена: сохраняем уже выполненные tool-вызовы (персистентность
|
|
818
|
+
// tool-chain) — иначе timeline вызовов пропадает из истории.
|
|
819
|
+
await this._persistToolChain(session.id, streamId, providerName, effectiveAgentRole, chunkModel ?? finalModel, allRawToolCalls, allToolCalls, allIntermediateTexts, allReasoning);
|
|
820
|
+
// Баг: накопленный текст ВСЕХ итераций (завершённые + текущая
|
|
821
|
+
// незавершённая), а не только последней. Иначе при стопе в истории
|
|
822
|
+
// остаётся обрывок на случайном месте, хотя пользователь видел весь
|
|
823
|
+
// текст в UI до стопа (тексты завершённых итераций лежат в
|
|
824
|
+
// allIntermediateTexts и раньше не попадали в content).
|
|
825
|
+
const partialContent = this._joinAccumulatedTexts(allIntermediateTexts, accumulatedContent);
|
|
826
|
+
// При отмене НЕ кладём allIntermediateTexts в metadata
|
|
827
|
+
// (intermediate_text_blocks): они уже вошли в partialContent, иначе
|
|
828
|
+
// UI (режим 'bottom') склеит их с content повторно — дубль.
|
|
829
|
+
if (partialContent || accumulatedReasoning || allToolCalls.length > 0) {
|
|
830
|
+
await this._saveAssistantMessage(session.id, partialContent, streamId, effectiveAgentRole, finalUsage, chunkModel, accumulatedReasoning, debugCollector, true, providerName, MessageRole.ASSISTANT, undefined, false, allToolCalls, undefined);
|
|
627
831
|
}
|
|
628
832
|
publishChunk('stream.cancelled', {
|
|
629
833
|
stream_id: streamId,
|
|
630
|
-
content:
|
|
834
|
+
content: partialContent,
|
|
631
835
|
});
|
|
632
836
|
this._cancellationTokens.delete(streamId);
|
|
633
837
|
return {};
|
|
@@ -644,6 +848,7 @@ export class ChatOrchestrator {
|
|
|
644
848
|
if (accumulatedContent) {
|
|
645
849
|
allIntermediateTexts.push({ text: accumulatedContent, iteration: toolIteration });
|
|
646
850
|
}
|
|
851
|
+
allRawToolCalls.push({ iteration: toolIteration, calls: toolCalls });
|
|
647
852
|
// Tool calls detected — execute them
|
|
648
853
|
debugCollector.captureRawModelResponse(`[tool_calls iteration ${toolIteration}]: ${JSON.stringify(toolCalls)}`);
|
|
649
854
|
// Add assistant message with tool calls to currentMessages
|
|
@@ -677,36 +882,11 @@ export class ChatOrchestrator {
|
|
|
677
882
|
else {
|
|
678
883
|
toolArgs = func.arguments || {};
|
|
679
884
|
}
|
|
680
|
-
// Doom loop detection
|
|
681
|
-
const doomKey = `${toolName}:${JSON.stringify(toolArgs)}`;
|
|
682
|
-
const doomCount = (doomLoopTracker.get(doomKey) || 0) + 1;
|
|
683
|
-
doomLoopTracker.set(doomKey, doomCount);
|
|
684
|
-
if (doomCount >= MAX_DOOM_LOOP_COUNT) {
|
|
685
|
-
warning('ORCHESTRATOR', `Doom loop detected: ${toolName} called ${doomCount} times with same args`);
|
|
686
|
-
currentMessages.push({
|
|
687
|
-
id: '',
|
|
688
|
-
role: 'tool',
|
|
689
|
-
content: `Ошибка: инструмент "${toolName}" вызван ${doomCount} раз(а) с одинаковыми аргументами. Цикл прерван.`,
|
|
690
|
-
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
691
|
-
disabled: false,
|
|
692
|
-
pinned: false,
|
|
693
|
-
branchId: '',
|
|
694
|
-
toolCallId: tcId,
|
|
695
|
-
});
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
const tcStartedAt = new Date().toISOString();
|
|
699
|
-
publishChunk('tool_call.started', {
|
|
700
|
-
stream_id: streamId,
|
|
701
|
-
session_id: session.id,
|
|
702
|
-
call_id: tcId,
|
|
703
|
-
name: toolName,
|
|
704
|
-
arguments: toolArgs,
|
|
705
|
-
iteration: toolIteration,
|
|
706
|
-
});
|
|
707
885
|
const toolDef = this._toolRegistry?.get(toolName);
|
|
708
886
|
let toolResult;
|
|
709
|
-
// AD-029: гранулярные permissions — проверяем
|
|
887
|
+
// AD-029: гранулярные permissions — проверяем ДО doom loop, чтобы
|
|
888
|
+
// deny-повторы не блокировались безликим «Цикл прерван», а получали
|
|
889
|
+
// полноценный feedback (tool_call.result) с объяснением политики.
|
|
710
890
|
// AD-025: учитываем текущий режим Plan/Act и строгость плана.
|
|
711
891
|
const permissionDecision = permissionEvaluator.check({
|
|
712
892
|
toolName,
|
|
@@ -717,12 +897,48 @@ export class ChatOrchestrator {
|
|
|
717
897
|
planMode,
|
|
718
898
|
planApproved,
|
|
719
899
|
mcpToolSettings: session.settings.mcpToolSettings,
|
|
900
|
+
shellPolicy: this._getProtectedPaths() ?? undefined,
|
|
901
|
+
protectedPaths: this._getProtectedPaths() ?? undefined,
|
|
902
|
+
protectedPathsEnabled: this._getProtectedPathsEnabled(),
|
|
903
|
+
});
|
|
904
|
+
// Doom loop detection — только для вызовов, НЕ заблокированных
|
|
905
|
+
// политикой. Deny-вызовы исключены: каждая попытка должна получать
|
|
906
|
+
// понятный feedback с объяснением причины запрета, а не «Цикл
|
|
907
|
+
// прерван». Механика для повторяющихся легальных вызовов сохраняется.
|
|
908
|
+
if (permissionDecision.action !== 'deny') {
|
|
909
|
+
const doomKey = `${toolName}:${JSON.stringify(toolArgs)}`;
|
|
910
|
+
const doomCount = (doomLoopTracker.get(doomKey) || 0) + 1;
|
|
911
|
+
doomLoopTracker.set(doomKey, doomCount);
|
|
912
|
+
if (doomCount >= MAX_DOOM_LOOP_COUNT) {
|
|
913
|
+
warning('ORCHESTRATOR', `Doom loop detected: ${toolName} called ${doomCount} times with same args`);
|
|
914
|
+
currentMessages.push({
|
|
915
|
+
id: '',
|
|
916
|
+
role: 'tool',
|
|
917
|
+
content: sm('tool.doom_loop', { tool: toolName, count: String(doomCount) }),
|
|
918
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
919
|
+
disabled: false,
|
|
920
|
+
pinned: false,
|
|
921
|
+
branchId: '',
|
|
922
|
+
toolCallId: tcId,
|
|
923
|
+
});
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const tcStartedAt = new Date().toISOString();
|
|
928
|
+
publishChunk('tool_call.started', {
|
|
929
|
+
stream_id: streamId,
|
|
930
|
+
session_id: session.id,
|
|
931
|
+
call_id: tcId,
|
|
932
|
+
name: toolName,
|
|
933
|
+
arguments: toolArgs,
|
|
934
|
+
iteration: toolIteration,
|
|
720
935
|
});
|
|
721
936
|
let executeTool = permissionDecision.action === 'allow';
|
|
722
937
|
let deniedReason;
|
|
723
938
|
let userDenied = false;
|
|
724
939
|
if (permissionDecision.action === 'deny') {
|
|
725
|
-
deniedReason = permissionDecision
|
|
940
|
+
deniedReason = this._securityMessage(permissionDecision, 'en')
|
|
941
|
+
?? `Permission denied: ${permissionDecision.reason}`;
|
|
726
942
|
}
|
|
727
943
|
else if (permissionDecision.action === 'ask') {
|
|
728
944
|
const approval = await this._approvalManager.waitForDecision(session.id, {
|
|
@@ -730,24 +946,27 @@ export class ChatOrchestrator {
|
|
|
730
946
|
callId: tcId,
|
|
731
947
|
toolName,
|
|
732
948
|
arguments: toolArgs,
|
|
733
|
-
reason: permissionDecision
|
|
949
|
+
reason: this._securityMessage(permissionDecision, 'en')
|
|
950
|
+
?? permissionDecision.reason,
|
|
734
951
|
}, effectivePermissionConfig.askTimeoutMs);
|
|
735
952
|
if (approval.approved) {
|
|
736
953
|
executeTool = true;
|
|
737
954
|
// «Разрешить в сессии» (once:false) — добавляем allow-правило для
|
|
738
955
|
// тула в permission-конфиг сессии, чтобы не спрашивать повторно.
|
|
739
956
|
if (approval.once === false) {
|
|
740
|
-
effectivePermissionConfig = await this._grantSessionToolAllow(session, toolName);
|
|
957
|
+
effectivePermissionConfig = await this._grantSessionToolAllow(session, toolName, toolArgs, workingDir);
|
|
741
958
|
permissionEvaluator = new PermissionEvaluator(effectivePermissionConfig);
|
|
742
959
|
}
|
|
743
960
|
}
|
|
744
961
|
else {
|
|
745
962
|
const why = approval.timeout
|
|
746
|
-
? '
|
|
963
|
+
? sm('permission.ask_timeout')
|
|
747
964
|
: approval.noSubscribers
|
|
748
|
-
? '
|
|
749
|
-
: '
|
|
750
|
-
|
|
965
|
+
? sm('permission.ask_no_subscribers')
|
|
966
|
+
: sm('permission.ask_denied');
|
|
967
|
+
const baseDeny = this._securityMessage(permissionDecision, 'en')
|
|
968
|
+
?? `Permission denied: ${permissionDecision.reason}`;
|
|
969
|
+
deniedReason = `${baseDeny} (${why})`;
|
|
751
970
|
userDenied = !approval.timeout && !approval.noSubscribers;
|
|
752
971
|
}
|
|
753
972
|
}
|
|
@@ -761,24 +980,29 @@ export class ChatOrchestrator {
|
|
|
761
980
|
matched_rule: permissionDecision.matchedRule,
|
|
762
981
|
});
|
|
763
982
|
toolResult = {
|
|
764
|
-
content:
|
|
983
|
+
content: deniedReason ?? sm('tool.permission_denied_default'),
|
|
765
984
|
isError: true,
|
|
766
985
|
metadata: {
|
|
986
|
+
...(permissionDecision.messageId
|
|
987
|
+
? { i18n: i18nMeta(`security.${permissionDecision.messageId.replaceAll('-', '_')}`, permissionDecision.params) }
|
|
988
|
+
: { i18n: i18nMeta('tool.permission_denied_default') }),
|
|
767
989
|
permission: {
|
|
768
990
|
action: 'deny',
|
|
769
|
-
reason: deniedReason,
|
|
991
|
+
reason: deniedReason ?? '',
|
|
770
992
|
matched_rule: permissionDecision.matchedRule,
|
|
771
993
|
},
|
|
772
994
|
},
|
|
773
995
|
};
|
|
996
|
+
hadDeny = true;
|
|
774
997
|
}
|
|
775
998
|
else {
|
|
776
999
|
if (toolDef) {
|
|
777
1000
|
const parsed = toolDef.parameters.safeParse(toolArgs);
|
|
778
1001
|
if (!parsed.success) {
|
|
779
1002
|
toolResult = {
|
|
780
|
-
content:
|
|
1003
|
+
content: sm('tool.args_error', { tool: toolName, issues: parsed.error.issues.map(i => `${i.path.join('.') || 'root'}: ${i.message}`).join('; ') }),
|
|
781
1004
|
isError: true,
|
|
1005
|
+
metadata: { i18n: i18nMeta('tool.args_error', { tool: toolName }) },
|
|
782
1006
|
};
|
|
783
1007
|
}
|
|
784
1008
|
else {
|
|
@@ -786,7 +1010,10 @@ export class ChatOrchestrator {
|
|
|
786
1010
|
toolResult = await toolDef.execute(parsed.data, {
|
|
787
1011
|
sessionId: session.id,
|
|
788
1012
|
messageId: message.metadata?.stream_id || streamId,
|
|
789
|
-
|
|
1013
|
+
// Живой сигнал отмены: абортится как по /abort (cancel()),
|
|
1014
|
+
// так и по cancelRetry (внешний signal). Без этого тулы
|
|
1015
|
+
// (task/сабагент) не узнают о «Стоп» до своего завершения.
|
|
1016
|
+
abortSignal: toolAbortController.signal,
|
|
790
1017
|
agentRole: effectiveAgentRole,
|
|
791
1018
|
workingDir,
|
|
792
1019
|
dataDir: this._modelPriceLoader ? undefined : undefined,
|
|
@@ -808,37 +1035,64 @@ export class ChatOrchestrator {
|
|
|
808
1035
|
routerConfig: this._routerConfig ?? undefined,
|
|
809
1036
|
contextConfig: this._contextConfig ?? undefined,
|
|
810
1037
|
securityConfig: this._securityConfig,
|
|
1038
|
+
language: session.settings.language || 'ru',
|
|
811
1039
|
publish: (type, data) => publishChunk(type, data),
|
|
812
1040
|
streamId,
|
|
813
1041
|
sessionId: session.id,
|
|
1042
|
+
// ID собственного tool-вызова — для вложенности вызовов
|
|
1043
|
+
// саб-агента под блоком `task` в родительском стриме.
|
|
1044
|
+
callId: tcId,
|
|
814
1045
|
toolRegistry: this._toolRegistry,
|
|
815
1046
|
permissionConfig: effectivePermissionConfig,
|
|
1047
|
+
shellPolicy: this._getProtectedPaths() ?? undefined,
|
|
1048
|
+
protectedPaths: this._getProtectedPaths() ?? undefined,
|
|
1049
|
+
protectedPathsEnabled: this._getProtectedPathsEnabled(),
|
|
816
1050
|
},
|
|
817
1051
|
});
|
|
818
1052
|
}
|
|
819
1053
|
catch (execError) {
|
|
820
1054
|
toolResult = {
|
|
821
|
-
content:
|
|
1055
|
+
content: sm('tool.exec_error', { tool: toolName, error: String(execError) }),
|
|
822
1056
|
isError: true,
|
|
1057
|
+
metadata: { i18n: i18nMeta('tool.exec_error', { tool: toolName }) },
|
|
823
1058
|
};
|
|
824
1059
|
}
|
|
825
1060
|
}
|
|
826
1061
|
}
|
|
827
1062
|
else {
|
|
828
1063
|
toolResult = {
|
|
829
|
-
content:
|
|
1064
|
+
content: sm('tool.not_found', { tool: toolName }),
|
|
830
1065
|
isError: true,
|
|
1066
|
+
metadata: { i18n: i18nMeta('tool.not_found', { tool: toolName }) },
|
|
831
1067
|
};
|
|
832
1068
|
}
|
|
833
1069
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1070
|
+
// Отмена пришла ВО ВРЕМЯ исполнения тула (например task/сабагент).
|
|
1071
|
+
// Фиксируем результат (цепочка тулов сохранится в истории), публикуем
|
|
1072
|
+
// tool_call.result (UI видит, чем тул завершился) и прекращаем
|
|
1073
|
+
// дальнейшие тулы/итерации — обработка завершится как отмена (ниже),
|
|
1074
|
+
// а не как обычное завершение с «ответ модели пуст».
|
|
1075
|
+
if (cancelToken.cancelled || toolAbortController.signal.aborted) {
|
|
1076
|
+
allToolCalls.push({
|
|
1077
|
+
name: toolName,
|
|
1078
|
+
args: toolArgs,
|
|
1079
|
+
id: tcId,
|
|
1080
|
+
result: toolResult,
|
|
1081
|
+
started_at: tcStartedAt,
|
|
1082
|
+
completed_at: new Date().toISOString(),
|
|
1083
|
+
iteration: toolIteration,
|
|
1084
|
+
});
|
|
1085
|
+
publishChunk('tool_call.result', {
|
|
1086
|
+
stream_id: streamId,
|
|
1087
|
+
session_id: session.id,
|
|
1088
|
+
call_id: tcId,
|
|
1089
|
+
name: toolName,
|
|
1090
|
+
result: toolResult,
|
|
1091
|
+
iteration: toolIteration,
|
|
1092
|
+
});
|
|
1093
|
+
cancelledDuringTool = true;
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
842
1096
|
// IPI-защита: оборачиваем tool-контент по матрице доверия + детектируем
|
|
843
1097
|
// подозрительные паттерны (флаг/sanitize/block по конфигу).
|
|
844
1098
|
if (this._securityConfig.promptInjection.enabled && !toolResult.isError) {
|
|
@@ -858,10 +1112,11 @@ export class ChatOrchestrator {
|
|
|
858
1112
|
const applied = applyPolicy(toolResult.content, piConfig.mode, detection);
|
|
859
1113
|
if (applied.blocked) {
|
|
860
1114
|
toolResult = {
|
|
861
|
-
content:
|
|
1115
|
+
content: sm('security.injection_blocked', { tool: toolName, matches: detection.matches.slice(0, 5).join(', ') }),
|
|
862
1116
|
isError: true,
|
|
863
1117
|
metadata: {
|
|
864
1118
|
...toolResult.metadata,
|
|
1119
|
+
i18n: i18nMeta('security.injection_blocked', { tool: toolName }),
|
|
865
1120
|
injection_suspicious: true,
|
|
866
1121
|
injection_matches: detection.matches,
|
|
867
1122
|
},
|
|
@@ -888,6 +1143,18 @@ export class ChatOrchestrator {
|
|
|
888
1143
|
}
|
|
889
1144
|
}
|
|
890
1145
|
}
|
|
1146
|
+
// Публикуем результат ПОСЛЕ IPI-обёртки: живой UI (tool_call.result)
|
|
1147
|
+
// должен получать тот же контент, что сохраняется в историю
|
|
1148
|
+
// (metadata.tool_results и tool-chain tool-сообщения), иначе при
|
|
1149
|
+
// перезагрузке вид результата меняется.
|
|
1150
|
+
publishChunk('tool_call.result', {
|
|
1151
|
+
stream_id: streamId,
|
|
1152
|
+
session_id: session.id,
|
|
1153
|
+
call_id: tcId,
|
|
1154
|
+
name: toolName,
|
|
1155
|
+
result: toolResult,
|
|
1156
|
+
iteration: toolIteration,
|
|
1157
|
+
});
|
|
891
1158
|
// classify_conversation: сохраняем категорию в настройках сессии (AD-034)
|
|
892
1159
|
if (toolName === 'classify_conversation' && !toolResult.isError) {
|
|
893
1160
|
const cat = toolResult.metadata?.routingCategory || undefined;
|
|
@@ -902,20 +1169,19 @@ export class ChatOrchestrator {
|
|
|
902
1169
|
});
|
|
903
1170
|
}
|
|
904
1171
|
}
|
|
905
|
-
// AD-025/AD-
|
|
906
|
-
// чтобы
|
|
1172
|
+
// AD-025/AD-039: обновляем локальный режим после plan_enter/plan_exit,
|
|
1173
|
+
// чтобы ограничения, system prompt и список тулов применились к
|
|
1174
|
+
// следующим тулам/итерациям.
|
|
907
1175
|
if (!toolResult.isError && toolName === 'plan_enter') {
|
|
908
1176
|
sessionMode = 'plan';
|
|
909
|
-
finalSystemPrompt = this._buildPlanSystemPrompt(finalSystemPrompt, session, planMode, sessionMode, planApproved);
|
|
1177
|
+
finalSystemPrompt = await this._buildPlanSystemPrompt(finalSystemPrompt, session, planMode, sessionMode, planApproved);
|
|
1178
|
+
tools = this._filterToolsByMode(baseTools, session, sessionMode, planMode, planApproved);
|
|
910
1179
|
}
|
|
911
1180
|
else if (!toolResult.isError && toolName === 'plan_exit') {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
sessionMode = 'act';
|
|
917
|
-
}
|
|
918
|
-
finalSystemPrompt = this._buildPlanSystemPrompt(finalSystemPrompt, session, planMode, sessionMode, planApproved, planContent);
|
|
1181
|
+
planApproved = true;
|
|
1182
|
+
sessionMode = 'act';
|
|
1183
|
+
finalSystemPrompt = await this._buildPlanSystemPrompt(finalSystemPrompt, session, planMode, sessionMode, planApproved);
|
|
1184
|
+
tools = this._filterToolsByMode(baseTools, session, sessionMode, planMode, planApproved);
|
|
919
1185
|
}
|
|
920
1186
|
allToolCalls.push({
|
|
921
1187
|
name: toolName,
|
|
@@ -946,6 +1212,12 @@ export class ChatOrchestrator {
|
|
|
946
1212
|
break;
|
|
947
1213
|
}
|
|
948
1214
|
}
|
|
1215
|
+
// Отмена между тулами итерации (например во время IPI-обработки) —
|
|
1216
|
+
// тоже завершаем как отмену, а не идём на следующую LLM-итерацию.
|
|
1217
|
+
if (!stopAfterDeny && (cancelToken.cancelled || toolAbortController.signal.aborted)) {
|
|
1218
|
+
cancelledDuringTool = true;
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
949
1221
|
publishChunk('tool_call.loop', {
|
|
950
1222
|
stream_id: streamId,
|
|
951
1223
|
session_id: session.id,
|
|
@@ -956,14 +1228,45 @@ export class ChatOrchestrator {
|
|
|
956
1228
|
if (stopAfterDeny)
|
|
957
1229
|
break;
|
|
958
1230
|
}
|
|
1231
|
+
// Отмена во время исполнения тула: завершаем ровно как ветку отмены
|
|
1232
|
+
// (persist tool-chain + stream.cancelled), а не как обычное завершение
|
|
1233
|
+
// с подставленным «Не удалось выполнить запрос: ответ модели пуст».
|
|
1234
|
+
if (cancelledDuringTool) {
|
|
1235
|
+
await this._persistToolChain(session.id, streamId, providerName, effectiveAgentRole, finalModel, allRawToolCalls, allToolCalls, allIntermediateTexts, allReasoning);
|
|
1236
|
+
const partialContent = this._joinAccumulatedTexts(allIntermediateTexts, '');
|
|
1237
|
+
// Промежуточные тексты уже вошли в partialContent — как в ветке отмены
|
|
1238
|
+
// выше, НЕ дублируем их в metadata.intermediate_text_blocks.
|
|
1239
|
+
if (partialContent || allToolCalls.length > 0) {
|
|
1240
|
+
await this._saveAssistantMessage(session.id, partialContent, streamId, effectiveAgentRole, finalUsage, finalModel, allReasoning.map((r) => r.text).join('\n\n---\n\n'), debugCollector, true, providerName, MessageRole.ASSISTANT, undefined, false, allToolCalls, undefined);
|
|
1241
|
+
}
|
|
1242
|
+
publishChunk('stream.cancelled', {
|
|
1243
|
+
stream_id: streamId,
|
|
1244
|
+
content: partialContent,
|
|
1245
|
+
});
|
|
1246
|
+
this._cancellationTokens.delete(streamId);
|
|
1247
|
+
return {};
|
|
1248
|
+
}
|
|
959
1249
|
// 7. Handle max iterations exceeded
|
|
960
1250
|
if (toolIteration >= maxIterations && !finalContent) {
|
|
961
|
-
finalContent = '
|
|
1251
|
+
finalContent = sm('session.max_iterations');
|
|
1252
|
+
fallbackI18n = i18nMeta('session.max_iterations');
|
|
962
1253
|
}
|
|
963
1254
|
// AD-029: пользователь отклонил ask — обработка завершена. Фиксируем
|
|
964
1255
|
// причину в финальном сообщении (история tool_result уже собрана).
|
|
965
1256
|
if (stopAfterDeny && !finalContent) {
|
|
966
|
-
|
|
1257
|
+
const reason = denyStopReason ?? sm('session.stopped_reason_denied');
|
|
1258
|
+
finalContent = sm('session.stopped', { reason });
|
|
1259
|
+
fallbackI18n = i18nMeta('session.stopped', { reason });
|
|
1260
|
+
}
|
|
1261
|
+
// Защита от пустого финального ответа: если цикл завершился (в т.ч.
|
|
1262
|
+
// через doom loop / лимит итераций / модель вернула пустой контент)
|
|
1263
|
+
// с пустым finalContent — подставляем осмысленное сообщение, чтобы
|
|
1264
|
+
// в истории не оставалось пустого assistant-сообщения.
|
|
1265
|
+
if (!finalContent) {
|
|
1266
|
+
finalContent = hadDeny
|
|
1267
|
+
? sm('session.denied_blocked')
|
|
1268
|
+
: sm('session.empty_response');
|
|
1269
|
+
fallbackI18n = i18nMeta(hadDeny ? 'session.denied_blocked' : 'session.empty_response');
|
|
967
1270
|
}
|
|
968
1271
|
// 7.5. Quality check + escalation (AD-034)
|
|
969
1272
|
let qaPass;
|
|
@@ -985,7 +1288,7 @@ export class ChatOrchestrator {
|
|
|
985
1288
|
reason: 'escalation_fallback',
|
|
986
1289
|
strategy: 'default',
|
|
987
1290
|
};
|
|
988
|
-
const fbProvider = this._router.getProvider(fbDecision);
|
|
1291
|
+
const fbProvider = this._router.getProvider(fbDecision, undefined, effectiveReasoningLevel);
|
|
989
1292
|
if (fbProvider) {
|
|
990
1293
|
// Перевызываем LLM с fallback-моделью (без стриминга в UI)
|
|
991
1294
|
try {
|
|
@@ -996,7 +1299,7 @@ export class ChatOrchestrator {
|
|
|
996
1299
|
if (fbResponse.content) {
|
|
997
1300
|
finalContent = fbResponse.content;
|
|
998
1301
|
qaPass = true;
|
|
999
|
-
qaReason =
|
|
1302
|
+
qaReason = sm('router.escalation', { provider: fallbackProvider, model: fallbackModel, reason: qaResult.reason });
|
|
1000
1303
|
if (fbResponse.usage)
|
|
1001
1304
|
finalUsage = fbResponse.usage;
|
|
1002
1305
|
if (fbResponse.model)
|
|
@@ -1029,9 +1332,15 @@ export class ChatOrchestrator {
|
|
|
1029
1332
|
});
|
|
1030
1333
|
}
|
|
1031
1334
|
}
|
|
1335
|
+
// 7.8. Персистентность tool-chain (Вариант B): настоящие пары
|
|
1336
|
+
// «assistant с toolCalls + tool-результаты» в session.messages, чтобы
|
|
1337
|
+
// _sessionMessagesToLlm на следующем запросе выдавал OpenAI-совместимый
|
|
1338
|
+
// диалог. Промежуточные сообщения помечаются metadata.tool_chain=true —
|
|
1339
|
+
// UI их скрывает (визуализация уже есть в финальном сообщении).
|
|
1340
|
+
await this._persistToolChain(session.id, streamId, providerName, effectiveAgentRole, finalModel, allRawToolCalls, allToolCalls, allIntermediateTexts, allReasoning);
|
|
1032
1341
|
// 8. Save final assistant message
|
|
1033
1342
|
const reasoningJoined = allReasoning.map((r) => r.text).join('\n\n---\n\n');
|
|
1034
|
-
const savedContent = await this._saveAssistantMessage(session.id, finalContent, streamId, effectiveAgentRole, finalUsage, finalModel, reasoningJoined, debugCollector, false, providerName, MessageRole.ASSISTANT, undefined, false, allToolCalls, allIntermediateTexts, qaPass, qaReason);
|
|
1343
|
+
const savedContent = await this._saveAssistantMessage(session.id, finalContent, streamId, effectiveAgentRole, finalUsage, finalModel, reasoningJoined, debugCollector, false, providerName, MessageRole.ASSISTANT, undefined, false, allToolCalls, allIntermediateTexts, qaPass, qaReason, fallbackI18n ? { i18n: fallbackI18n } : undefined);
|
|
1035
1344
|
// Получаем актуальную сессию после сохранения
|
|
1036
1345
|
const updatedSession = await this._sessionManager.getSession(session.id);
|
|
1037
1346
|
const doneData = {
|
|
@@ -1066,7 +1375,23 @@ export class ChatOrchestrator {
|
|
|
1066
1375
|
if (updatedSession) {
|
|
1067
1376
|
const userMsgCount = updatedSession.messages.filter((m) => m.role === MessageRole.USER && !m.metadata?.synthetic).length;
|
|
1068
1377
|
if (userMsgCount === 1) {
|
|
1069
|
-
|
|
1378
|
+
const titleAgentDef = this._agentRegistry?.get('title');
|
|
1379
|
+
let titleProvider = provider;
|
|
1380
|
+
if (titleAgentDef?.reasoningLevel && providerName) {
|
|
1381
|
+
const tp = this._getProvider(providerName, updatedSession, titleAgentDef.reasoningLevel);
|
|
1382
|
+
if (tp)
|
|
1383
|
+
titleProvider = tp;
|
|
1384
|
+
}
|
|
1385
|
+
autoTitleSession(updatedSession, this._sessionManager, titleProvider)
|
|
1386
|
+
.then((title) => {
|
|
1387
|
+
if (title) {
|
|
1388
|
+
this._eventBus.publish(updatedSession.id, 'session.updated', {
|
|
1389
|
+
session_id: updatedSession.id,
|
|
1390
|
+
name: title,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
})
|
|
1394
|
+
.catch(() => { });
|
|
1070
1395
|
}
|
|
1071
1396
|
}
|
|
1072
1397
|
// 9. Check context optimization triggers
|
|
@@ -1135,11 +1460,8 @@ export class ChatOrchestrator {
|
|
|
1135
1460
|
catch (e) {
|
|
1136
1461
|
const errMsg = String(e);
|
|
1137
1462
|
warning('ORCHESTRATOR', `Unexpected error: ${errMsg}`);
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
errorType: 'internal',
|
|
1141
|
-
canRetry: false,
|
|
1142
|
-
};
|
|
1463
|
+
this._cancellationTokens.delete(streamId);
|
|
1464
|
+
return this._handleLlmFailure(session.id, streamId, e, publishChunk);
|
|
1143
1465
|
}
|
|
1144
1466
|
}
|
|
1145
1467
|
cancel(streamId) {
|
|
@@ -1147,12 +1469,118 @@ export class ChatOrchestrator {
|
|
|
1147
1469
|
if (token)
|
|
1148
1470
|
token.cancel();
|
|
1149
1471
|
}
|
|
1150
|
-
|
|
1472
|
+
/**
|
|
1473
|
+
* Регистрация внешнего cancel-токена (resume саб-агента): позволяет отменить
|
|
1474
|
+
* фоновый прогон обычным путём — orchestrator.cancel(streamId) из
|
|
1475
|
+
* POST /sessions/:id/abort (токен ищется в общей карте по streamId).
|
|
1476
|
+
*/
|
|
1477
|
+
registerCancelToken(streamId, token) {
|
|
1478
|
+
this._cancellationTokens.set(streamId, token);
|
|
1479
|
+
}
|
|
1480
|
+
unregisterCancelToken(streamId) {
|
|
1481
|
+
this._cancellationTokens.delete(streamId);
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* B2: возобновление прерванной/ошибочной дочерней сессии саб-агента.
|
|
1485
|
+
* Валидация и захват блокировки — синхронно (ошибки маппятся route-ом в
|
|
1486
|
+
* 400/404/409), выполнение — фоновое (mimo очереди: у дочерней сессии нет
|
|
1487
|
+
* своего QueueProcessor). Защита от двойного resume — acquireLock +
|
|
1488
|
+
* currentStream (последний также делает сессию abort-совместимой).
|
|
1489
|
+
*/
|
|
1490
|
+
async startSubagentResume(childSessionId) {
|
|
1491
|
+
const deps = this._subagentResumeDeps();
|
|
1492
|
+
const { child, parent } = await validateSubagentResume(deps, childSessionId);
|
|
1493
|
+
const streamId = generateStreamId();
|
|
1494
|
+
const processId = `resume:${streamId}`;
|
|
1495
|
+
// Атомарный захват — защита от гонки параллельных resume-запросов
|
|
1496
|
+
const acquired = await this._sessionManager.acquireLock(childSessionId, processId);
|
|
1497
|
+
if (!acquired) {
|
|
1498
|
+
throw new SubagentResumeError('LOCKED', sm('task.resume_locked', { id: childSessionId }));
|
|
1499
|
+
}
|
|
1500
|
+
// currentStream — маркер активности для UI и abort-механики
|
|
1501
|
+
await this._sessionManager.withLock(childSessionId, async (s) => {
|
|
1502
|
+
s.currentStream = {
|
|
1503
|
+
streamId,
|
|
1504
|
+
message: sm('task.resume_marker'),
|
|
1505
|
+
agentRole: s.settings.agentRole,
|
|
1506
|
+
startedAt: nowISO(),
|
|
1507
|
+
cancelled: false,
|
|
1508
|
+
};
|
|
1509
|
+
await this._sessionManager.saveSession(s);
|
|
1510
|
+
});
|
|
1511
|
+
// Отмена: orchestrator.cancel(streamId) → resumeAbort.abort() →
|
|
1512
|
+
// runSubagent видит отмену (стрим + backoff-пауза) → finishAborted.
|
|
1513
|
+
const resumeAbort = new AbortController();
|
|
1514
|
+
const cancelToken = {
|
|
1515
|
+
cancelled: false,
|
|
1516
|
+
cancel: () => {
|
|
1517
|
+
cancelToken.cancelled = true;
|
|
1518
|
+
resumeAbort.abort();
|
|
1519
|
+
},
|
|
1520
|
+
};
|
|
1521
|
+
this.registerCancelToken(streamId, cancelToken);
|
|
1522
|
+
const freshChild = (await this._sessionManager.getSession(childSessionId)) ?? child;
|
|
1523
|
+
void runSubagentResume(deps, {
|
|
1524
|
+
child: freshChild,
|
|
1525
|
+
parent,
|
|
1526
|
+
streamId,
|
|
1527
|
+
abortSignal: resumeAbort.signal,
|
|
1528
|
+
})
|
|
1529
|
+
.catch((err) => {
|
|
1530
|
+
warning('ORCHESTRATOR', `subagent resume failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1531
|
+
this._eventBus.publish(childSessionId, 'stream.error', {
|
|
1532
|
+
stream_id: streamId,
|
|
1533
|
+
error: String(err),
|
|
1534
|
+
error_type: 'resume_failed',
|
|
1535
|
+
can_retry: false,
|
|
1536
|
+
});
|
|
1537
|
+
})
|
|
1538
|
+
.finally(async () => {
|
|
1539
|
+
this.unregisterCancelToken(streamId);
|
|
1540
|
+
try {
|
|
1541
|
+
await this._sessionManager.withLock(childSessionId, async (s) => {
|
|
1542
|
+
s.currentStream = undefined;
|
|
1543
|
+
await this._sessionManager.saveSession(s);
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
catch { /* сессия могла быть удалена */ }
|
|
1547
|
+
try {
|
|
1548
|
+
await this._sessionManager.releaseLock(childSessionId, processId);
|
|
1549
|
+
}
|
|
1550
|
+
catch { /* ignore */ }
|
|
1551
|
+
});
|
|
1552
|
+
return {
|
|
1553
|
+
status: 'started',
|
|
1554
|
+
child_session_id: childSessionId,
|
|
1555
|
+
parent_session_id: parent.id,
|
|
1556
|
+
stream_id: streamId,
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
/** Зависимости resume из приватных резолверов оркестратора. */
|
|
1560
|
+
_subagentResumeDeps() {
|
|
1561
|
+
return {
|
|
1562
|
+
sessionManager: this._sessionManager,
|
|
1563
|
+
eventBus: this._eventBus,
|
|
1564
|
+
agentRegistry: this._agentRegistry ?? { get: () => undefined },
|
|
1565
|
+
toolRegistry: this._toolRegistry ?? undefined,
|
|
1566
|
+
contextConfig: this._contextConfig ?? undefined,
|
|
1567
|
+
contextOptimizer: this._optimizer ?? undefined,
|
|
1568
|
+
routerConfig: this._routerConfig ?? undefined,
|
|
1569
|
+
securityConfig: this._securityConfig,
|
|
1570
|
+
permissionConfig: this._permissionConfig,
|
|
1571
|
+
shellPolicy: this._getProtectedPaths() ?? undefined,
|
|
1572
|
+
protectedPaths: this._getProtectedPaths() ?? undefined,
|
|
1573
|
+
protectedPathsEnabled: this._getProtectedPathsEnabled(),
|
|
1574
|
+
getProvider: (child) => this._getProvider(child.settings.provider || '', child, child.settings.reasoningLevel),
|
|
1575
|
+
getWorkingDir: (child) => this._resolveWorkingDir(child),
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
async _handleDebugSystemMessage(session, message, signal, publishChunk, debugCollector, streamId, reasoningLevel) {
|
|
1151
1579
|
const providerName = message.provider || session.settings.provider || '';
|
|
1152
|
-
const provider = this._getProvider(providerName, session);
|
|
1580
|
+
const provider = this._getProvider(providerName, session, reasoningLevel);
|
|
1153
1581
|
if (!provider) {
|
|
1154
1582
|
return {
|
|
1155
|
-
error:
|
|
1583
|
+
error: sm('router.provider_not_found', { provider: providerName || sm('router.provider_unspecified') }),
|
|
1156
1584
|
errorType: 'provider_not_found',
|
|
1157
1585
|
canRetry: false,
|
|
1158
1586
|
};
|
|
@@ -1207,7 +1635,8 @@ export class ChatOrchestrator {
|
|
|
1207
1635
|
}
|
|
1208
1636
|
catch (e) {
|
|
1209
1637
|
warning('ORCHESTRATOR', `Debug system message stream error: ${e}`);
|
|
1210
|
-
|
|
1638
|
+
this._cancellationTokens.delete(streamId);
|
|
1639
|
+
return this._handleLlmFailure(session.id, streamId, e, publishChunk);
|
|
1211
1640
|
}
|
|
1212
1641
|
if (cancelToken.cancelled) {
|
|
1213
1642
|
publishChunk('stream.cancelled', { stream_id: streamId, content: accumulatedContent });
|
|
@@ -1287,7 +1716,8 @@ export class ChatOrchestrator {
|
|
|
1287
1716
|
}
|
|
1288
1717
|
catch (e) {
|
|
1289
1718
|
warning('ORCHESTRATOR', `Summarization stream error: ${e}`);
|
|
1290
|
-
|
|
1719
|
+
this._cancellationTokens.delete(streamId);
|
|
1720
|
+
return this._handleLlmFailure(session.id, streamId, e, publishChunk);
|
|
1291
1721
|
}
|
|
1292
1722
|
if (cancelToken.cancelled) {
|
|
1293
1723
|
publishChunk('stream.cancelled', { stream_id: streamId, content: accumulatedContent });
|
|
@@ -1343,13 +1773,19 @@ export class ChatOrchestrator {
|
|
|
1343
1773
|
this._cancellationTokens.delete(streamId);
|
|
1344
1774
|
return {};
|
|
1345
1775
|
}
|
|
1346
|
-
_getProvider(providerName, session) {
|
|
1776
|
+
_getProvider(providerName, session, reasoningLevel) {
|
|
1347
1777
|
const modelName = session.settings.model || '';
|
|
1348
1778
|
const sharedConfig = this._providerLoader(providerName);
|
|
1349
1779
|
if (!sharedConfig)
|
|
1350
1780
|
return null;
|
|
1351
1781
|
try {
|
|
1352
|
-
const
|
|
1782
|
+
const effectiveModel = modelName || sharedConfig.defaultModel || '';
|
|
1783
|
+
const contextWindow = this._resolveModelContextWindow(effectiveModel);
|
|
1784
|
+
const provider = ProviderFactory.create(providerName, {
|
|
1785
|
+
...toCoreProviderConfig(sharedConfig, effectiveModel, reasoningLevel ?? session.settings.reasoningLevel),
|
|
1786
|
+
maxTokens: session.settings.maxTokens,
|
|
1787
|
+
numCtx: contextWindow > 0 ? contextWindow : undefined,
|
|
1788
|
+
}, session.settings.temperature);
|
|
1353
1789
|
return this._wrapWithGateway(provider, session);
|
|
1354
1790
|
}
|
|
1355
1791
|
catch (e) {
|
|
@@ -1357,6 +1793,39 @@ export class ChatOrchestrator {
|
|
|
1357
1793
|
return null;
|
|
1358
1794
|
}
|
|
1359
1795
|
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Размер контекстного окна модели: приоритет у сохранённого значения
|
|
1798
|
+
* (ModelConfig.contextWindow через оптимизатор), иначе справочник.
|
|
1799
|
+
*/
|
|
1800
|
+
_resolveModelContextWindow(modelName) {
|
|
1801
|
+
const window = this._optimizer?.getContextWindow(modelName);
|
|
1802
|
+
if (window && window > 0)
|
|
1803
|
+
return window;
|
|
1804
|
+
return lookupContextWindow(modelName).window;
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Публикует context.limit_warning, когда оценка токенов контекста достигла
|
|
1808
|
+
* порога (CONTEXT_LIMIT_WARNING_PERCENT) от окна модели. Уведомление не
|
|
1809
|
+
* блокирует вызов LLM — это fire-and-forget сигнал для UI.
|
|
1810
|
+
*/
|
|
1811
|
+
_publishContextLimitWarning(sessionId, modelName, messages) {
|
|
1812
|
+
if (!this._optimizer)
|
|
1813
|
+
return;
|
|
1814
|
+
const contextWindow = this._resolveModelContextWindow(modelName);
|
|
1815
|
+
if (!(contextWindow > 0))
|
|
1816
|
+
return;
|
|
1817
|
+
const estimatedTokens = this._optimizer.estimateTokens(messages);
|
|
1818
|
+
if (estimatedTokens <= 0)
|
|
1819
|
+
return;
|
|
1820
|
+
const percent = Math.round((estimatedTokens / contextWindow) * 100);
|
|
1821
|
+
if (percent >= CONTEXT_LIMIT_WARNING_PERCENT) {
|
|
1822
|
+
this._eventBus.publish(sessionId, 'context.limit_warning', {
|
|
1823
|
+
percent,
|
|
1824
|
+
context_window: contextWindow,
|
|
1825
|
+
estimated_tokens: estimatedTokens,
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1360
1829
|
/**
|
|
1361
1830
|
* AD-036: оборачивает провайдер в GatewayProvider с привязкой к сессии.
|
|
1362
1831
|
* Если gateway выключен — возвращает провайдер как есть (обратная совместимость).
|
|
@@ -1383,7 +1852,7 @@ export class ChatOrchestrator {
|
|
|
1383
1852
|
* Выбор провайдера и модели через роутер (AD-034).
|
|
1384
1853
|
* Если роутер недоступен — fallback на _getProvider().
|
|
1385
1854
|
*/
|
|
1386
|
-
_routeProvider(session, message) {
|
|
1855
|
+
_routeProvider(session, message, reasoningLevel) {
|
|
1387
1856
|
if (this._router && session.settings.routingEnabled !== false) {
|
|
1388
1857
|
const toolCallNames = message.toolCalls?.map((tc) => {
|
|
1389
1858
|
const func = tc.function;
|
|
@@ -1394,7 +1863,8 @@ export class ChatOrchestrator {
|
|
|
1394
1863
|
userMessage: message.content,
|
|
1395
1864
|
toolCalls: toolCallNames,
|
|
1396
1865
|
});
|
|
1397
|
-
|
|
1866
|
+
const routedWindow = this._resolveModelContextWindow(decision.modelName);
|
|
1867
|
+
let provider = this._router.getProvider(decision, undefined, reasoningLevel ?? session.settings.reasoningLevel, session.settings.maxTokens, routedWindow > 0 ? routedWindow : undefined);
|
|
1398
1868
|
if (provider) {
|
|
1399
1869
|
provider = this._wrapWithGateway(provider, session);
|
|
1400
1870
|
return {
|
|
@@ -1408,7 +1878,7 @@ export class ChatOrchestrator {
|
|
|
1408
1878
|
// (например, категория не имеет маппинга в routingContent),
|
|
1409
1879
|
// используем стандартный _getProvider() для провайдера сессии
|
|
1410
1880
|
const fallbackName = session.settings.provider || '';
|
|
1411
|
-
provider = this._getProvider(fallbackName, session);
|
|
1881
|
+
provider = this._getProvider(fallbackName, session, reasoningLevel);
|
|
1412
1882
|
if (provider) {
|
|
1413
1883
|
return {
|
|
1414
1884
|
provider,
|
|
@@ -1420,7 +1890,7 @@ export class ChatOrchestrator {
|
|
|
1420
1890
|
}
|
|
1421
1891
|
// Fallback: старый метод (если роутер недоступен или все пути выше не сработали)
|
|
1422
1892
|
const providerName = message.provider || session.settings.provider || '';
|
|
1423
|
-
const provider = this._getProvider(providerName, session);
|
|
1893
|
+
const provider = this._getProvider(providerName, session, reasoningLevel);
|
|
1424
1894
|
if (!provider)
|
|
1425
1895
|
return null;
|
|
1426
1896
|
return {
|
|
@@ -1468,6 +1938,7 @@ export class ChatOrchestrator {
|
|
|
1468
1938
|
branchId: '',
|
|
1469
1939
|
toolUse: isOwnMessage ? msg.toolCalls : undefined,
|
|
1470
1940
|
toolCallId: isOwnMessage ? msg.toolCallId : undefined,
|
|
1941
|
+
reasoning: isOwnMessage && llmRole === MessageRole.ASSISTANT ? msg.reasoning : undefined,
|
|
1471
1942
|
agentRole: msg.agentRole,
|
|
1472
1943
|
});
|
|
1473
1944
|
}
|
|
@@ -1503,7 +1974,86 @@ export class ChatOrchestrator {
|
|
|
1503
1974
|
return {};
|
|
1504
1975
|
}
|
|
1505
1976
|
}
|
|
1506
|
-
|
|
1977
|
+
/**
|
|
1978
|
+
* Персистентность tool-chain (Вариант B): настоящие пары
|
|
1979
|
+
* «assistant с toolCalls + tool-результаты» в session.messages, чтобы
|
|
1980
|
+
* _sessionMessagesToLlm на следующем запросе выдавал OpenAI-совместимый
|
|
1981
|
+
* диалог. Промежуточные сообщения помечаются metadata.tool_chain=true —
|
|
1982
|
+
* UI их скрывает (визуализация уже есть в финальном сообщении).
|
|
1983
|
+
* Вызывается и в успешном пути, и при отмене (уже выполненные итерации).
|
|
1984
|
+
*/
|
|
1985
|
+
async _persistToolChain(sessionId, streamId, providerName, agentRole, finalModel, allRawToolCalls, allToolCalls, allIntermediateTexts, allReasoning) {
|
|
1986
|
+
for (const raw of allRawToolCalls) {
|
|
1987
|
+
const iteration = raw.iteration;
|
|
1988
|
+
const iterText = allIntermediateTexts.find((t) => t.iteration === iteration)?.text ?? '';
|
|
1989
|
+
const iterReasoning = allReasoning.find((r) => r.iteration === iteration)?.text;
|
|
1990
|
+
const assistantMsg = {
|
|
1991
|
+
id: generateId(),
|
|
1992
|
+
role: MessageRole.ASSISTANT,
|
|
1993
|
+
seq: 0,
|
|
1994
|
+
origin: 'local',
|
|
1995
|
+
parts: [makeTextPart(iterText)],
|
|
1996
|
+
content: iterText,
|
|
1997
|
+
toolCalls: raw.calls,
|
|
1998
|
+
createdAt: nowISO(),
|
|
1999
|
+
disabled: false,
|
|
2000
|
+
pinned: false,
|
|
2001
|
+
hidden: false,
|
|
2002
|
+
model: finalModel,
|
|
2003
|
+
provider: providerName,
|
|
2004
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
2005
|
+
agentRole,
|
|
2006
|
+
metadata: { stream_id: streamId, tool_chain: true },
|
|
2007
|
+
...(iterReasoning ? { reasoning: iterReasoning } : {}),
|
|
2008
|
+
};
|
|
2009
|
+
await this._sessionManager.addMessage(sessionId, assistantMsg);
|
|
2010
|
+
for (const tc of allToolCalls.filter((t) => t.iteration === iteration)) {
|
|
2011
|
+
const toolContent = String(tc.result?.content ?? '');
|
|
2012
|
+
// metadata ToolResult (в т.ч. i18n) пробрасывается в metadata сообщения —
|
|
2013
|
+
// иначе локализационные ключи не доходят до истории и UI.
|
|
2014
|
+
const toolResultMeta = tc.result?.metadata;
|
|
2015
|
+
const toolMsg = {
|
|
2016
|
+
id: generateId(),
|
|
2017
|
+
role: MessageRole.TOOL,
|
|
2018
|
+
seq: 0,
|
|
2019
|
+
origin: 'local',
|
|
2020
|
+
parts: [makeTextPart(toolContent)],
|
|
2021
|
+
content: toolContent,
|
|
2022
|
+
toolCallId: tc.id,
|
|
2023
|
+
createdAt: nowISO(),
|
|
2024
|
+
disabled: false,
|
|
2025
|
+
pinned: false,
|
|
2026
|
+
hidden: false,
|
|
2027
|
+
model: finalModel,
|
|
2028
|
+
provider: providerName,
|
|
2029
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
2030
|
+
agentRole,
|
|
2031
|
+
metadata: {
|
|
2032
|
+
...(toolResultMeta ?? {}),
|
|
2033
|
+
stream_id: streamId,
|
|
2034
|
+
tool_chain: true,
|
|
2035
|
+
tool_name: tc.name,
|
|
2036
|
+
args: tc.args,
|
|
2037
|
+
},
|
|
2038
|
+
};
|
|
2039
|
+
await this._sessionManager.addMessage(sessionId, toolMsg);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Склейка накопленного текста: все завершённые итерации (allIntermediateTexts)
|
|
2045
|
+
* + текст текущей (незавершённой) итерации. Пустые фрагменты пропускаются.
|
|
2046
|
+
* Используется при отмене (abort), чтобы в истории сохранялся полный
|
|
2047
|
+
* частичный ответ, а не только обрывок последней итерации.
|
|
2048
|
+
*/
|
|
2049
|
+
_joinAccumulatedTexts(intermediateTexts, current) {
|
|
2050
|
+
const parts = [...intermediateTexts.map((t) => t.text), current]
|
|
2051
|
+
.filter((t) => t && t.trim().length > 0);
|
|
2052
|
+
return parts.join('\n\n');
|
|
2053
|
+
}
|
|
2054
|
+
async _saveAssistantMessage(sessionId, content, streamId, agentRole, usage, model, reasoning, debugCollector, isCancelled, provider, role = MessageRole.ASSISTANT, summaryOf, skipEvent = false, toolResults, intermediateTextBlocks, qaPass, qaReason,
|
|
2055
|
+
/** Дополнительные поля metadata (например metadata.i18n фоллбек-статусов). */
|
|
2056
|
+
metadataExtra) {
|
|
1507
2057
|
const finalContent = content;
|
|
1508
2058
|
if (!isCancelled && content) {
|
|
1509
2059
|
debugCollector.captureRawModelResponse(content);
|
|
@@ -1512,7 +2062,7 @@ export class ChatOrchestrator {
|
|
|
1512
2062
|
debugCollector.captureReasoning(reasoning);
|
|
1513
2063
|
}
|
|
1514
2064
|
const debugInfo = debugCollector.getDebugInfo();
|
|
1515
|
-
const metadata = { stream_id: streamId };
|
|
2065
|
+
const metadata = { stream_id: streamId, ...metadataExtra };
|
|
1516
2066
|
if (toolResults && toolResults.length > 0) {
|
|
1517
2067
|
metadata.tool_results = toolResults;
|
|
1518
2068
|
}
|
|
@@ -1600,51 +2150,111 @@ export class ChatOrchestrator {
|
|
|
1600
2150
|
return finalContent;
|
|
1601
2151
|
}
|
|
1602
2152
|
_classifyError(errMsg) {
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
return 'rate_limit';
|
|
1606
|
-
if (lower.includes('timeout') || lower.includes('timed out'))
|
|
1607
|
-
return 'timeout';
|
|
1608
|
-
if (lower.includes('502') || lower.includes('503') || lower.includes('bad gateway'))
|
|
1609
|
-
return 'server_error';
|
|
1610
|
-
if (lower.includes('context length') || lower.includes('too long'))
|
|
1611
|
-
return 'context_length';
|
|
1612
|
-
if (lower.includes('401') || lower.includes('403') || lower.includes('auth'))
|
|
1613
|
-
return 'auth_error';
|
|
1614
|
-
if (lower.includes('model') && (lower.includes('not found') || lower.includes('invalid')))
|
|
1615
|
-
return 'model_not_found';
|
|
1616
|
-
return 'unknown';
|
|
2153
|
+
// Общий хелпер (используется также retry-циклом саб-агента) — таксономия единая
|
|
2154
|
+
return classifyLlmError(errMsg);
|
|
1617
2155
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
2156
|
+
/**
|
|
2157
|
+
* Человекочитаемое описание ошибки LLM (таймаут/обрыв соединения, сетевая
|
|
2158
|
+
* ошибка). Для остальных ошибок — исходный текст.
|
|
2159
|
+
*/
|
|
2160
|
+
_formatLlmError(error) {
|
|
2161
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
2162
|
+
const lower = raw.toLowerCase();
|
|
2163
|
+
if (lower.includes('abort') || lower.includes('operation was aborted') || lower.includes('timed out') || lower.includes('timeout')) {
|
|
2164
|
+
return sm('router.timeout');
|
|
2165
|
+
}
|
|
2166
|
+
if (lower.includes('fetch failed') || lower.includes('network') || lower.includes('econnrefused') || lower.includes('enotfound') || lower.includes('eai_again')) {
|
|
2167
|
+
return sm('router.network_error');
|
|
2168
|
+
}
|
|
2169
|
+
return raw;
|
|
1626
2170
|
}
|
|
1627
2171
|
/**
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
2172
|
+
* Единая обработка ошибки LLM-стрима: пишет last_error в user-сообщение
|
|
2173
|
+
* сессии (по stream_id) и возвращает ProcessMessageResult. Публикация
|
|
2174
|
+
* финального исхода (stream.retry для recoverable / stream.error для
|
|
2175
|
+
* non-recoverable) — зона ответственности QueueProcessor, чтобы не было
|
|
2176
|
+
* двойной публикации stream.error. Если ошибка recoverable — session
|
|
2177
|
+
* разблокируется на уровне QueueProcessor после исчерпания попыток; сессия
|
|
2178
|
+
* не остаётся заблокированной навсегда.
|
|
1631
2179
|
*/
|
|
1632
|
-
async
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
2180
|
+
async _handleLlmFailure(sessionId, streamId, error, _publishChunk) {
|
|
2181
|
+
const rawError = error instanceof Error ? error.message : String(error);
|
|
2182
|
+
const humanError = this._formatLlmError(error);
|
|
2183
|
+
// ContextLengthExceededError несёт русское сообщение («Контекстное окно
|
|
2184
|
+
// превышено») — текстовый классификатор его не распознаёт, поэтому тип
|
|
2185
|
+
// определяем по классу ошибки, чтобы UI показал дружелюбное сообщение.
|
|
2186
|
+
const errorType = error instanceof ContextLengthExceededError
|
|
2187
|
+
? 'context_length'
|
|
2188
|
+
: this._classifyError(rawError);
|
|
2189
|
+
const canRetry = isLlmErrorRecoverable(error, rawError, errorType);
|
|
2190
|
+
try {
|
|
2191
|
+
await this._sessionManager.withLock(sessionId, async (s) => {
|
|
2192
|
+
const userMsg = s.messages.find((m) => m.role === MessageRole.USER && m.metadata?.stream_id === streamId);
|
|
2193
|
+
if (userMsg) {
|
|
2194
|
+
userMsg.metadata = { ...userMsg.metadata, last_error: humanError };
|
|
2195
|
+
await this._sessionManager.saveSession(s);
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
catch (err) {
|
|
2200
|
+
warning('ORCHESTRATOR', `Не удалось записать last_error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1639
2201
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
2202
|
+
return { error: humanError, errorType, canRetry };
|
|
2203
|
+
}
|
|
2204
|
+
/** Строит permission-конфиг с добавленным грантом для заданной базы правил. */
|
|
2205
|
+
_applyToolAllow(cfg, toolName, paths) {
|
|
2206
|
+
const base = cfg ?? {
|
|
2207
|
+
rules: { ...this._permissionConfig.rules },
|
|
2208
|
+
askTimeoutMs: this._permissionConfig.askTimeoutMs,
|
|
1643
2209
|
};
|
|
2210
|
+
const rules = { ...base.rules };
|
|
2211
|
+
if (paths.length > 0) {
|
|
2212
|
+
if (rules[toolName] !== 'allow') {
|
|
2213
|
+
const existing = (typeof rules[toolName] === 'object' && rules[toolName])
|
|
2214
|
+
? { ...rules[toolName] }
|
|
2215
|
+
: {};
|
|
2216
|
+
for (const p of paths)
|
|
2217
|
+
existing[p] = 'allow';
|
|
2218
|
+
rules[toolName] = existing;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
else {
|
|
2222
|
+
rules[toolName] = 'allow';
|
|
2223
|
+
}
|
|
2224
|
+
return { ...base, rules };
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* AD-029: «Разрешить в сессии» (once:false). Если у вызова есть файловые
|
|
2228
|
+
* параметры — одобряет КОНКРЕТНЫЕ абсолютные пути (правило-объект
|
|
2229
|
+
* `rules[tool] = { path: 'allow' }`), иначе (shell/task/mcp) — весь тул.
|
|
2230
|
+
* Если тул уже разрешён полностью ('allow') — не понижает до пути.
|
|
2231
|
+
*/
|
|
2232
|
+
async _grantSessionToolAllow(session, toolName, toolArgs, workingDir) {
|
|
2233
|
+
const paths = extractPaths(toolName, toolArgs, workingDir);
|
|
2234
|
+
const next = this._applyToolAllow(session.settings.permission, toolName, paths);
|
|
1644
2235
|
await this._sessionManager.withLock(session.id, async (s) => {
|
|
1645
2236
|
s.settings.permission = next;
|
|
1646
2237
|
await this._sessionManager.saveSession(s);
|
|
2238
|
+
this._eventBus.publish(session.id, 'session.updated', {
|
|
2239
|
+
session_id: session.id,
|
|
2240
|
+
settings: s.settings,
|
|
2241
|
+
});
|
|
1647
2242
|
});
|
|
2243
|
+
// AD-029: «Разрешить в сессии» для сабагентской (дочерней) сессии
|
|
2244
|
+
// распространяется и на родителя (parentId): путь в правиле — абсолютный
|
|
2245
|
+
// канонический, поэтому грант совпадёт при чтении того же файла и родителем,
|
|
2246
|
+
// и ребёнком. Родительский грант обновляет и effectivePermissionConfig, и
|
|
2247
|
+
// PermissionEvaluator при следующей обработке (сессия сохраняется в store).
|
|
2248
|
+
if (session.parentId) {
|
|
2249
|
+
await this._sessionManager.withLock(session.parentId, async (parent) => {
|
|
2250
|
+
parent.settings.permission = this._applyToolAllow(parent.settings.permission, toolName, paths);
|
|
2251
|
+
await this._sessionManager.saveSession(parent);
|
|
2252
|
+
this._eventBus.publish(parent.id, 'session.updated', {
|
|
2253
|
+
session_id: parent.id,
|
|
2254
|
+
settings: parent.settings,
|
|
2255
|
+
});
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
1648
2258
|
return next;
|
|
1649
2259
|
}
|
|
1650
2260
|
}
|