plugin-agent-orchestrator 1.0.19 → 1.0.21

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.
Files changed (100) hide show
  1. package/dist/client/hooks/useRunEventStream.d.ts +22 -0
  2. package/dist/client/index.d.ts +1 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/externalVersion.js +6 -6
  5. package/dist/server/collections/agent-execution-spans.js +24 -0
  6. package/dist/server/collections/agent-loop-runs.js +36 -0
  7. package/dist/server/collections/orchestrator-config.js +14 -0
  8. package/dist/server/migrations/20260601000000-add-token-fields.d.ts +7 -0
  9. package/dist/server/migrations/20260601000000-add-token-fields.js +101 -0
  10. package/dist/server/plugin.js +47 -0
  11. package/dist/server/resources/agent-loop.js +33 -25
  12. package/dist/server/resources/tracing.js +5 -8
  13. package/dist/server/services/AgentHarness.d.ts +2 -0
  14. package/dist/server/services/AgentHarness.js +56 -90
  15. package/dist/server/services/AgentLoopController.d.ts +33 -20
  16. package/dist/server/services/AgentLoopController.js +164 -125
  17. package/dist/server/services/AgentLoopRepository.js +16 -34
  18. package/dist/server/services/AgentLoopService.d.ts +28 -18
  19. package/dist/server/services/AgentLoopService.js +7 -1
  20. package/dist/server/services/AgentPlannerService.js +5 -25
  21. package/dist/server/services/AgentRegistryService.d.ts +8 -0
  22. package/dist/server/services/AgentRegistryService.js +34 -24
  23. package/dist/server/services/CircuitBreaker.d.ts +40 -0
  24. package/dist/server/services/CircuitBreaker.js +120 -0
  25. package/dist/server/services/ContextAggregator.d.ts +45 -0
  26. package/dist/server/services/ContextAggregator.js +201 -0
  27. package/dist/server/services/ExecutionSpanService.js +2 -5
  28. package/dist/server/services/RunEventBus.d.ts +9 -0
  29. package/dist/server/services/RunEventBus.js +73 -0
  30. package/dist/server/services/TokenTracker.d.ts +62 -0
  31. package/dist/server/services/TokenTracker.js +173 -0
  32. package/dist/server/skill-hub/plugin.js +6 -6
  33. package/dist/server/skill-hub/tasks/SkillExecutionTask.js +6 -6
  34. package/dist/server/tools/agent-loop.d.ts +8 -8
  35. package/dist/server/tools/agent-loop.js +30 -63
  36. package/dist/server/tools/delegate-task.js +14 -72
  37. package/dist/server/tools/orchestrator-plan.d.ts +6 -6
  38. package/dist/server/tools/orchestrator-plan.js +10 -47
  39. package/dist/server/types.d.ts +47 -0
  40. package/dist/server/types.js +24 -0
  41. package/dist/server/utils/ctx-utils.d.ts +30 -0
  42. package/dist/server/utils/ctx-utils.js +152 -0
  43. package/dist/server/utils/logging.d.ts +6 -0
  44. package/dist/server/utils/logging.js +86 -0
  45. package/package.json +44 -44
  46. package/src/client/AgentRunsTab.tsx +764 -764
  47. package/src/client/HarnessProfilesTab.tsx +247 -247
  48. package/src/client/OrchestratorSettings.tsx +106 -106
  49. package/src/client/RulesTab.tsx +716 -716
  50. package/src/client/hooks/useRunEventStream.ts +76 -0
  51. package/src/client/index.tsx +2 -1
  52. package/src/client/plugin.tsx +27 -27
  53. package/src/client/skill-hub/components/LoopSettings.tsx +331 -331
  54. package/src/client/skill-hub/index.tsx +51 -51
  55. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +99 -99
  56. package/src/client/skill-hub/tools/SkillHubCard.tsx +109 -109
  57. package/src/client/skill-hub/tools/loopTemplates.ts +52 -52
  58. package/src/client/skill-hub/tools/registerSkillLoopCards.ts +58 -58
  59. package/src/client/tools/PlanApprovalCard.tsx +175 -175
  60. package/src/client/tools/registerOrchestratorCards.ts +7 -7
  61. package/src/server/__tests__/agent-loop-controller.test.ts +375 -0
  62. package/src/server/__tests__/circuit-breaker.test.ts +169 -0
  63. package/src/server/__tests__/context-aggregator.test.ts +222 -0
  64. package/src/server/__tests__/parallel-execution.test.ts +318 -0
  65. package/src/server/__tests__/smoke.test.ts +120 -0
  66. package/src/server/collections/agent-execution-spans.ts +24 -0
  67. package/src/server/collections/agent-harness-profiles.ts +59 -59
  68. package/src/server/collections/agent-loop-events.ts +71 -71
  69. package/src/server/collections/agent-loop-runs.ts +38 -1
  70. package/src/server/collections/agent-loop-steps.ts +144 -144
  71. package/src/server/collections/orchestrator-config.ts +14 -0
  72. package/src/server/collections/skill-executions.ts +106 -106
  73. package/src/server/collections/skill-loop-configs.ts +65 -65
  74. package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -30
  75. package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +142 -142
  76. package/src/server/migrations/20260601000000-add-token-fields.ts +89 -0
  77. package/src/server/plugin.ts +53 -0
  78. package/src/server/resources/agent-loop.ts +21 -12
  79. package/src/server/resources/tracing.ts +3 -7
  80. package/src/server/services/AgentHarness.ts +78 -116
  81. package/src/server/services/AgentLoopController.ts +197 -122
  82. package/src/server/services/AgentLoopRepository.ts +9 -25
  83. package/src/server/services/AgentLoopService.ts +13 -1
  84. package/src/server/services/AgentPlanValidator.ts +73 -73
  85. package/src/server/services/AgentPlannerService.ts +2 -25
  86. package/src/server/services/AgentRegistryService.ts +40 -31
  87. package/src/server/services/CircuitBreaker.ts +116 -0
  88. package/src/server/services/ContextAggregator.ts +239 -0
  89. package/src/server/services/ExecutionSpanService.ts +2 -4
  90. package/src/server/services/RunEventBus.ts +45 -0
  91. package/src/server/services/TokenTracker.ts +209 -0
  92. package/src/server/skill-hub/plugin.ts +898 -897
  93. package/src/server/skill-hub/tasks/SkillExecutionTask.ts +460 -458
  94. package/src/server/tools/agent-loop.ts +18 -57
  95. package/src/server/tools/delegate-task.ts +11 -93
  96. package/src/server/tools/orchestrator-plan.ts +26 -50
  97. package/src/server/tools/skill-execute.ts +160 -160
  98. package/src/server/types.ts +55 -0
  99. package/src/server/utils/ctx-utils.ts +118 -0
  100. package/src/server/utils/logging.ts +63 -0
@@ -1,160 +1,160 @@
1
- import { parseJsonText } from '../skill-hub/utils/json-fields';
2
-
3
- export function createSkillExecuteTool(plugin: any) {
4
- return {
5
- scope: 'CUSTOM',
6
- execution: 'backend',
7
- defaultPermission: 'ASK',
8
-
9
- introduction: {
10
- title: 'Skill Hub - Universal Skill Executor',
11
- about:
12
- 'A universal gateway to execute predefined specialized skills (data processing, complex calculations, file generation, etc.) inside a secure Python/Node.js sandbox.',
13
- },
14
-
15
- definition: {
16
- name: 'skill_hub_execute',
17
- description: `A universal gateway to execute various predefined specialized skills (e.g., data transformation, calculations, document/presentation generation) in an isolated sandbox.
18
- HOW TO USE THIS TOOL:
19
- 1. If you don't know the exact 'skillName' or its required 'input' schema, first call this tool with { "action": "list" } to discover all available skills.
20
- 2. For complex workflow skills, call { "action": "describe", "skillName": "<exact_skill_name>" } to load the full instructions before execution.
21
- 3. To run a skill, call this tool with { "action": "execute", "skillName": "<exact_skill_name>", "input": { <parameters> } }.
22
- CRITICAL: Do NOT guess or hallucinate the 'input' object. You MUST strictly provide the parameters matching the JSON schema defined for that specific skill.
23
- The skill's output may contain text results, structured JSON data, or download URLs for generated files.
24
- IMPORTANT: If the skill returns file download URLs, you MUST format them as clickable Markdown links (e.g., [Download filename.ext](/api/attachments/...)) in your final response to the user.`,
25
- schema: {
26
- type: 'object',
27
- properties: {
28
- action: {
29
- type: 'string',
30
- enum: ['list', 'describe', 'execute'],
31
- description: '"list" to see available skills, "describe" to load full instructions, "execute" to run one',
32
- },
33
- skillName: {
34
- type: 'string',
35
- description: 'Skill name (required for "execute" action)',
36
- },
37
- input: {
38
- type: 'object',
39
- description: 'Input parameters matching the skill inputSchema',
40
- },
41
- },
42
- required: ['action'],
43
- },
44
- },
45
-
46
- async invoke(ctx: any, args: Record<string, any>, _id?: string) {
47
- plugin.app.logger.info(`[skill-execute] Tool invoked with action: ${args.action}, skillName: ${args.skillName}`);
48
-
49
- // Action: list available skills
50
- if (args.action === 'list') {
51
- const skills = await plugin.db.getRepository('skillDefinitions').find({
52
- filter: { enabled: true },
53
- fields: ['name', 'title', 'description', 'language', 'inputSchema', 'instructions', 'storageType'],
54
- });
55
-
56
- const skillList = skills.map((s: any) => ({
57
- name: s.get('name'),
58
- title: s.get('title'),
59
- description: s.get('description'),
60
- language: s.get('language'),
61
- inputSchema: parseJsonText(s.get('inputSchema'), null),
62
- hasInstructions: !!s.get('instructions') || s.get('storageType') === 'plugin',
63
- storageType: s.get('storageType'),
64
- }));
65
-
66
- return {
67
- status: 'success',
68
- content: JSON.stringify({ skills: skillList }),
69
- };
70
- }
71
-
72
- // Action: describe one skill with full workflow instructions
73
- if (args.action === 'describe') {
74
- if (!args.skillName) {
75
- return { status: 'error', content: 'Missing skillName parameter' };
76
- }
77
-
78
- const skill = await plugin.db.getRepository('skillDefinitions').findOne({
79
- filter: { name: args.skillName, enabled: true },
80
- });
81
-
82
- if (!skill) {
83
- return {
84
- status: 'error',
85
- content: `Skill "${args.skillName}" not found or disabled`,
86
- };
87
- }
88
-
89
- const instructions =
90
- typeof plugin.getSkillInstructions === 'function'
91
- ? await plugin.getSkillInstructions(skill)
92
- : skill.get('instructions');
93
-
94
- return {
95
- status: 'success',
96
- content: JSON.stringify({
97
- name: skill.get('name'),
98
- title: skill.get('title'),
99
- description: skill.get('description'),
100
- language: skill.get('language'),
101
- inputSchema: parseJsonText(skill.get('inputSchema'), null),
102
- packages: parseJsonText(skill.get('packages'), []),
103
- storageType: skill.get('storageType'),
104
- storageUrl: skill.get('storageUrl'),
105
- instructions,
106
- }),
107
- };
108
- }
109
-
110
- // Action: execute skill
111
- if (args.action === 'execute') {
112
- if (!args.skillName) {
113
- return { status: 'error', content: 'Missing skillName parameter' };
114
- }
115
-
116
- const skill = await plugin.db.getRepository('skillDefinitions').findOne({
117
- filter: { name: args.skillName, enabled: true },
118
- });
119
-
120
- if (!skill) {
121
- return {
122
- status: 'error',
123
- content: `Skill "${args.skillName}" not found or disabled`,
124
- };
125
- }
126
-
127
- try {
128
- const result = await plugin.executeSkill(skill, args.input || {}, ctx);
129
-
130
- return {
131
- status: result.status === 'succeeded' ? 'success' : 'error',
132
- content: JSON.stringify({
133
- message:
134
- result.status === 'succeeded'
135
- ? `Executed successfully. ${result.files?.length || 0} file(s) generated.`
136
- : `Failed: ${result.stderr}`,
137
- stdout: result.stdout,
138
- stderr: result.stderr,
139
- files: result.files,
140
- execId: result.execId,
141
- agentLoopRunId: result.agentLoopRunId,
142
- agentLoopStepId: result.agentLoopStepId,
143
- durationMs: result.durationMs,
144
- }),
145
- };
146
- } catch (error) {
147
- return {
148
- status: 'error',
149
- content: `Execution error: ${error instanceof Error ? error.message : String(error)}`,
150
- };
151
- }
152
- }
153
-
154
- return {
155
- status: 'error',
156
- content: `Unknown action "${args.action}". Use "list", "describe", or "execute".`,
157
- };
158
- },
159
- };
160
- }
1
+ import { parseJsonText } from '../skill-hub/utils/json-fields';
2
+
3
+ export function createSkillExecuteTool(plugin: any) {
4
+ return {
5
+ scope: 'CUSTOM',
6
+ execution: 'backend',
7
+ defaultPermission: 'ASK',
8
+
9
+ introduction: {
10
+ title: 'Skill Hub - Universal Skill Executor',
11
+ about:
12
+ 'A universal gateway to execute predefined specialized skills (data processing, complex calculations, file generation, etc.) inside a secure Python/Node.js sandbox.',
13
+ },
14
+
15
+ definition: {
16
+ name: 'skill_hub_execute',
17
+ description: `A universal gateway to execute various predefined specialized skills (e.g., data transformation, calculations, document/presentation generation) in an isolated sandbox.
18
+ HOW TO USE THIS TOOL:
19
+ 1. If you don't know the exact 'skillName' or its required 'input' schema, first call this tool with { "action": "list" } to discover all available skills.
20
+ 2. For complex workflow skills, call { "action": "describe", "skillName": "<exact_skill_name>" } to load the full instructions before execution.
21
+ 3. To run a skill, call this tool with { "action": "execute", "skillName": "<exact_skill_name>", "input": { <parameters> } }.
22
+ CRITICAL: Do NOT guess or hallucinate the 'input' object. You MUST strictly provide the parameters matching the JSON schema defined for that specific skill.
23
+ The skill's output may contain text results, structured JSON data, or download URLs for generated files.
24
+ IMPORTANT: If the skill returns file download URLs, you MUST format them as clickable Markdown links (e.g., [Download filename.ext](/api/attachments/...)) in your final response to the user.`,
25
+ schema: {
26
+ type: 'object',
27
+ properties: {
28
+ action: {
29
+ type: 'string',
30
+ enum: ['list', 'describe', 'execute'],
31
+ description: '"list" to see available skills, "describe" to load full instructions, "execute" to run one',
32
+ },
33
+ skillName: {
34
+ type: 'string',
35
+ description: 'Skill name (required for "execute" action)',
36
+ },
37
+ input: {
38
+ type: 'object',
39
+ description: 'Input parameters matching the skill inputSchema',
40
+ },
41
+ },
42
+ required: ['action'],
43
+ },
44
+ },
45
+
46
+ async invoke(ctx: any, args: Record<string, any>, _id?: string) {
47
+ plugin.app.logger.info(`[skill-execute] Tool invoked with action: ${args.action}, skillName: ${args.skillName}`);
48
+
49
+ // Action: list available skills
50
+ if (args.action === 'list') {
51
+ const skills = await plugin.db.getRepository('skillDefinitions').find({
52
+ filter: { enabled: true },
53
+ fields: ['name', 'title', 'description', 'language', 'inputSchema', 'instructions', 'storageType'],
54
+ });
55
+
56
+ const skillList = skills.map((s: any) => ({
57
+ name: s.get('name'),
58
+ title: s.get('title'),
59
+ description: s.get('description'),
60
+ language: s.get('language'),
61
+ inputSchema: parseJsonText(s.get('inputSchema'), null),
62
+ hasInstructions: !!s.get('instructions') || s.get('storageType') === 'plugin',
63
+ storageType: s.get('storageType'),
64
+ }));
65
+
66
+ return {
67
+ status: 'success',
68
+ content: JSON.stringify({ skills: skillList }),
69
+ };
70
+ }
71
+
72
+ // Action: describe one skill with full workflow instructions
73
+ if (args.action === 'describe') {
74
+ if (!args.skillName) {
75
+ return { status: 'error', content: 'Missing skillName parameter' };
76
+ }
77
+
78
+ const skill = await plugin.db.getRepository('skillDefinitions').findOne({
79
+ filter: { name: args.skillName, enabled: true },
80
+ });
81
+
82
+ if (!skill) {
83
+ return {
84
+ status: 'error',
85
+ content: `Skill "${args.skillName}" not found or disabled`,
86
+ };
87
+ }
88
+
89
+ const instructions =
90
+ typeof plugin.getSkillInstructions === 'function'
91
+ ? await plugin.getSkillInstructions(skill)
92
+ : skill.get('instructions');
93
+
94
+ return {
95
+ status: 'success',
96
+ content: JSON.stringify({
97
+ name: skill.get('name'),
98
+ title: skill.get('title'),
99
+ description: skill.get('description'),
100
+ language: skill.get('language'),
101
+ inputSchema: parseJsonText(skill.get('inputSchema'), null),
102
+ packages: parseJsonText(skill.get('packages'), []),
103
+ storageType: skill.get('storageType'),
104
+ storageUrl: skill.get('storageUrl'),
105
+ instructions,
106
+ }),
107
+ };
108
+ }
109
+
110
+ // Action: execute skill
111
+ if (args.action === 'execute') {
112
+ if (!args.skillName) {
113
+ return { status: 'error', content: 'Missing skillName parameter' };
114
+ }
115
+
116
+ const skill = await plugin.db.getRepository('skillDefinitions').findOne({
117
+ filter: { name: args.skillName, enabled: true },
118
+ });
119
+
120
+ if (!skill) {
121
+ return {
122
+ status: 'error',
123
+ content: `Skill "${args.skillName}" not found or disabled`,
124
+ };
125
+ }
126
+
127
+ try {
128
+ const result = await plugin.executeSkill(skill, args.input || {}, ctx);
129
+
130
+ return {
131
+ status: result.status === 'succeeded' ? 'success' : 'error',
132
+ content: JSON.stringify({
133
+ message:
134
+ result.status === 'succeeded'
135
+ ? `Executed successfully. ${result.files?.length || 0} file(s) generated.`
136
+ : `Failed: ${result.stderr}`,
137
+ stdout: result.stdout,
138
+ stderr: result.stderr,
139
+ files: result.files,
140
+ execId: result.execId,
141
+ agentLoopRunId: result.agentLoopRunId,
142
+ agentLoopStepId: result.agentLoopStepId,
143
+ durationMs: result.durationMs,
144
+ }),
145
+ };
146
+ } catch (error) {
147
+ return {
148
+ status: 'error',
149
+ content: `Execution error: ${error instanceof Error ? error.message : String(error)}`,
150
+ };
151
+ }
152
+ }
153
+
154
+ return {
155
+ status: 'error',
156
+ content: `Unknown action "${args.action}". Use "list", "describe", or "execute".`,
157
+ };
158
+ },
159
+ };
160
+ }
@@ -0,0 +1,55 @@
1
+ // ── Shared types for Agent Orchestrator ──────────────────────────────────
2
+
3
+ export interface TokenUsage {
4
+ inputTokens: number;
5
+ outputTokens: number;
6
+ totalTokens: number;
7
+ cost: number;
8
+ }
9
+
10
+ export interface BudgetConfig {
11
+ budgetMaxTokens?: number;
12
+ budgetMaxCost?: number;
13
+ }
14
+
15
+ export interface BudgetCheckResult {
16
+ allowed: boolean;
17
+ reason?: string;
18
+ }
19
+
20
+ export interface CircuitState {
21
+ failures: number;
22
+ lastFailureTime: number;
23
+ state: 'closed' | 'open' | 'half-open';
24
+ }
25
+
26
+ export interface TraceEvent {
27
+ type: string;
28
+ at: string;
29
+ title: string;
30
+ content?: string;
31
+ toolName?: string;
32
+ args?: any;
33
+ status?: string;
34
+ }
35
+
36
+ export interface DelegationLogData {
37
+ id?: number | string;
38
+ leaderUsername: string;
39
+ subAgentUsername: string;
40
+ toolName: string;
41
+ task: string;
42
+ context?: string;
43
+ result: string;
44
+ status: string;
45
+ depth: number;
46
+ durationMs: number;
47
+ error?: string;
48
+ trace?: TraceEvent[];
49
+ messages?: any[];
50
+ userId?: number | string;
51
+ }
52
+
53
+ export type CtxSnapshot = {
54
+ userId?: number;
55
+ };
@@ -0,0 +1,118 @@
1
+ // ── Shared context/state utility functions ─────────────────────────────
2
+
3
+ /** Normalize a record to a plain JS object. */
4
+ export function toPlain(record: any) {
5
+ return record?.toJSON?.() || record;
6
+ }
7
+
8
+ /** Coerce a value to a plain object (JSON parse if needed). */
9
+ export function asObject(value: any) {
10
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value;
11
+ if (typeof value === 'string' && value.trim()) {
12
+ try {
13
+ const parsed = JSON.parse(value);
14
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+ return {};
20
+ }
21
+
22
+ /** Coerce value to array. */
23
+ export function asArray(value: any): any[] {
24
+ return Array.isArray(value) ? value : [];
25
+ }
26
+
27
+ /** Trim text to max length with ellipsis suffix. */
28
+ export function trimText(value: any, max = 50000) {
29
+ let text = '';
30
+ if (typeof value === 'string') {
31
+ text = value;
32
+ } else if (value != null) {
33
+ try {
34
+ text = JSON.stringify(value);
35
+ } catch {
36
+ text = String(value);
37
+ }
38
+ }
39
+ return text.length > max ? `${text.slice(0, max)}\n...[truncated]` : text;
40
+ }
41
+
42
+ /** Get the current user id from ctx. */
43
+ export function currentUserId(ctx: any) {
44
+ return ctx?.state?.currentUser?.id || ctx?.auth?.user?.id;
45
+ }
46
+
47
+ /** Get action params values from ctx. */
48
+ export function valuesFromCtx(ctx: any) {
49
+ return ctx?.action?.params?.values || ctx?.request?.body || {};
50
+ }
51
+
52
+ /** Normalize raw employee username input (string | object → string | null). */
53
+ export function normalizeEmployeeUsername(raw: any) {
54
+ if (!raw) return null;
55
+ if (typeof raw === 'string') return raw;
56
+ return raw.username || raw.aiEmployeeUsername || raw.name || null;
57
+ }
58
+
59
+ /** Resolve session id from args or ctx. */
60
+ export function resolveSessionId(ctx: any, args: any) {
61
+ const v = valuesFromCtx(ctx);
62
+ return args?.sessionId || v.sessionId || ctx?.action?.params?.sessionId || ctx?.state?.sessionId;
63
+ }
64
+
65
+ /** Resolve message id from args or ctx. */
66
+ export function resolveMessageId(ctx: any, args: any) {
67
+ const v = valuesFromCtx(ctx);
68
+ return args?.messageId || v.messageId || ctx?.action?.params?.messageId;
69
+ }
70
+
71
+ /** Resolve leader employee username from args, ctx state, or conversation record. */
72
+ export async function resolveLeaderUsername(ctx: any, plugin: any, args: any) {
73
+ const v = valuesFromCtx(ctx);
74
+ const direct = normalizeEmployeeUsername(
75
+ args?.leaderUsername ||
76
+ ctx?._currentAIEmployee ||
77
+ ctx?.state?.currentAIEmployee ||
78
+ ctx?.runtime?.context?.currentAIEmployee ||
79
+ v.aiEmployee,
80
+ );
81
+ if (direct) return direct;
82
+
83
+ const sessionId = resolveSessionId(ctx, args);
84
+ if (!sessionId) return undefined;
85
+ try {
86
+ const repo = ctx?.db?.getRepository?.('aiConversations') || plugin.db.getRepository('aiConversations');
87
+ const conversation = await repo.findOne({ filter: { sessionId } });
88
+ return normalizeEmployeeUsername(conversation?.aiEmployeeUsername || conversation?.get?.('aiEmployeeUsername'));
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ }
93
+
94
+ /** Snapshot ctx user id for later use (avoids stale ctx). */
95
+ export function captureCtxSnapshot(ctx: any): { userId?: number } {
96
+ let userId: number | undefined;
97
+ try {
98
+ userId = ctx?.auth?.user?.id || ctx?.state?.currentUser?.id;
99
+ } catch {
100
+ // ctx already disposed
101
+ }
102
+ return { userId };
103
+ }
104
+
105
+ /** Normalize step type to a known value. */
106
+ export function normalizeStepType(value: any) {
107
+ return ['reasoning', 'skill', 'tool', 'sub_agent', 'verification'].includes(value) ? value : 'tool';
108
+ }
109
+
110
+ /** Normalize plan key from step input. */
111
+ export function normalizePlanKey(step: any, index: number) {
112
+ return String(step.planKey || step.key || step.id || `step_${index + 1}`);
113
+ }
114
+
115
+ /** Now ISO string. */
116
+ export function nowIso() {
117
+ return new Date().toISOString();
118
+ }
@@ -0,0 +1,63 @@
1
+ // ── Shared logging for delegation events ───────────────────────────────
2
+
3
+ import { DelegationLogData } from '../types';
4
+ import { trimText, toPlain } from './ctx-utils';
5
+
6
+ /**
7
+ * Log (or update) a delegation event in the orchestratorLogs collection.
8
+ * Used by both delegate-task.ts and AgentHarness.ts.
9
+ */
10
+ export async function logDelegation(ctx: any, plugin: any, data: DelegationLogData) {
11
+ try {
12
+ const logsRepo = plugin.db.getRepository('orchestratorLogs');
13
+ if (!logsRepo) {
14
+ plugin.app.log?.warn?.('[AgentOrchestrator] orchestratorLogs repository not found — skipping log');
15
+ return null;
16
+ }
17
+
18
+ let userId: number | string | undefined = data.userId;
19
+ if (userId == null) {
20
+ try {
21
+ userId = ctx?.auth?.user?.id || ctx?.state?.currentUser?.id;
22
+ } catch {
23
+ // ctx lifecycle ended
24
+ }
25
+ }
26
+
27
+ const values = {
28
+ leaderUsername: data.leaderUsername,
29
+ subAgentUsername: data.subAgentUsername,
30
+ toolName: data.toolName,
31
+ task: trimText(data.task, 10000),
32
+ context: trimText(data.context || '', 10000),
33
+ result: trimText(data.result || '', 50000),
34
+ status: data.status,
35
+ depth: data.depth,
36
+ durationMs: data.durationMs,
37
+ error: trimText(data.error || '', 10000),
38
+ trace: data.trace || [],
39
+ messages: data.messages || [],
40
+ userId,
41
+ updatedAt: new Date(),
42
+ };
43
+
44
+ if (data.id) {
45
+ await logsRepo.update({
46
+ filterByTk: data.id,
47
+ values,
48
+ });
49
+ return { id: data.id };
50
+ }
51
+
52
+ const record = await logsRepo.create({
53
+ values: {
54
+ ...values,
55
+ createdAt: new Date(),
56
+ },
57
+ });
58
+ return toPlain(record);
59
+ } catch (e) {
60
+ plugin.app.log?.warn?.('[AgentOrchestrator] Failed to log delegation event', e);
61
+ return null;
62
+ }
63
+ }