@saltcorn/copilot 0.8.1 → 0.8.2

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.
@@ -163,6 +163,13 @@ class GenerateTables {
163
163
  },
164
164
  },
165
165
  },
166
+ reused_table_names: {
167
+ type: "array",
168
+ items: { type: "string" },
169
+ description:
170
+ "Names of existing tables that are already complete and require no changes. " +
171
+ "List them here so the caller knows which tables were reused as-is. Do NOT repeat their field definitions in the tables array.",
172
+ },
166
173
  },
167
174
  };
168
175
  }
@@ -202,6 +209,12 @@ class GenerateTables {
202
209
  want to add or update. The system will automatically add new fields and update the settings of
203
210
  existing fields — it will not recreate or drop the table.
204
211
 
212
+ ## Reused tables
213
+
214
+ If an existing table is used by the application as-is (no new fields needed), do NOT repeat it
215
+ in the tables array. Instead, add its name to the reused_table_names array. This tells the
216
+ system to include it in the schema diagram without attempting to modify it.
217
+
205
218
  If a user requests creating a table with certain fields and the table already exists, automatically add any missing fields to that table. Do not ask the user for confirmation or prompt them again—just proceed with the table update.
206
219
 
207
220
  If a table has a ForeignKey field that references another table which does not yet exist in the
@@ -451,6 +464,7 @@ const buildMermaidMarkup = (tables) => {
451
464
  };
452
465
 
453
466
  module.exports = GenerateTables;
467
+ module.exports.buildMermaidMarkup = buildMermaidMarkup;
454
468
 
455
469
  /* todo
456
470
 
@@ -52,20 +52,27 @@ class GeneratePageSkill {
52
52
  }
53
53
  get userActions() {
54
54
  return {
55
- async build_copilot_page_gen({ user, name, title, description, html }) {
55
+ async build_copilot_page_gen({
56
+ user,
57
+ name,
58
+ title,
59
+ description,
60
+ html,
61
+ min_role = 100,
62
+ }) {
56
63
  const file = await File.from_contents(
57
64
  `${name}.html`,
58
65
  "text/html",
59
66
  html,
60
67
  user?.id,
61
- 100,
68
+ min_role
62
69
  );
63
70
 
64
71
  await Page.create({
65
72
  name,
66
73
  title,
67
74
  description,
68
- min_role: 100,
75
+ min_role,
69
76
  layout: { html_file: file.path_to_serve },
70
77
  });
71
78
  setTimeout(() => getState().refresh_pages(), 200);
@@ -114,7 +121,7 @@ class GeneratePageSkill {
114
121
 
115
122
  <script src="/static_assets/js/saltcorn-common.js"></script>
116
123
  <script src="/static_assets/js/saltcorn.js">
117
- `,
124
+ `
118
125
  );
119
126
  const html = str.includes("```html")
120
127
  ? str.split("```html")[1].split("```")[0]
@@ -126,6 +133,7 @@ class GeneratePageSkill {
126
133
  name: tool_call.input.name,
127
134
  title: tool_call.input.title,
128
135
  description: tool_call.input.description,
136
+ min_role: tool_call.input.min_role ?? 100,
129
137
  html,
130
138
  });
131
139
  return {
@@ -148,7 +156,7 @@ class GeneratePageSkill {
148
156
  add_user_action: {
149
157
  name: "build_copilot_page_gen",
150
158
  type: "button",
151
- label: "Save page "+tool_call.input.name,
159
+ label: "Save page " + tool_call.input.name,
152
160
  input: { html },
153
161
  },
154
162
  };
@@ -163,7 +171,7 @@ class GeneratePageSkill {
163
171
  response.includes("Unable to provide HTML for this page")
164
172
  )
165
173
  return response;
166
- if (
174
+ if (
167
175
  typeof response === "string" &&
168
176
  response.includes("The HTML code for the ")
169
177
  )
@@ -194,6 +202,12 @@ class GeneratePageSkill {
194
202
  "A longer description that is not visible but appears in the page header and is indexed by search engines",
195
203
  type: "string",
196
204
  },
205
+ min_role: {
206
+ description:
207
+ "Minimum role required to access this page. Use 1 for admin-only, 40 for staff and above, 80 for logged-in users and above, 100 for public. Set this based on the intended audience described in the task.",
208
+ type: "integer",
209
+ enum: [1, 40, 80, 100],
210
+ },
197
211
  page_type: {
198
212
  description:
199
213
  "The type of page to generate: a Marketing page if for promotional purposes, such as a landing page or a brouchure, with an appealing design. An Application page is simpler and an integrated part of the application",
@@ -6,6 +6,7 @@ const Field = require("@saltcorn/data/models/field");
6
6
  const User = require("@saltcorn/data/models/user");
7
7
  const Plugin = require("@saltcorn/data/models/plugin");
8
8
  const Role = require("@saltcorn/data/models/role");
9
+ const File = require("@saltcorn/data/models/file");
9
10
  const WorkflowStep = require("@saltcorn/data/models/workflow_step");
10
11
  const { getState } = require("@saltcorn/data/db/state");
11
12
 
@@ -167,6 +168,7 @@ with both the entity type and name, and the new JSON definition as a string as a
167
168
  "trigger",
168
169
  "plugin",
169
170
  "system-configuration-value",
171
+ "file",
170
172
  "type",
171
173
  ],
172
174
  },
@@ -317,6 +319,11 @@ with both the entity type and name, and the new JSON definition as a string as a
317
319
  }
318
320
  return v;
319
321
  }
322
+ case "file": {
323
+ const file = await File.findOne({ filename: input.entity_name });
324
+ if (!file) return `file not found`;
325
+ return { filename: file.filename, min_role_read: file.min_role_read };
326
+ }
320
327
  case "plugin": {
321
328
  const plugin = await Plugin.findOne({ name: input.entity_name });
322
329
  if (!plugin) return `plugin not found`;
@@ -473,6 +480,7 @@ with both the entity type and name, and the new JSON definition as a string as a
473
480
  "system-configuration-value",
474
481
  "module-configuration",
475
482
  "role",
483
+ "file",
476
484
  ],
477
485
  },
478
486
  entity_name: {
@@ -897,6 +905,13 @@ with both the entity type and name, and the new JSON definition as a string as a
897
905
  await getState().refresh_roles();
898
906
  return "Role created";
899
907
  }
908
+
909
+ case "file": {
910
+ const file = await File.findOne({ filename: input.entity_name });
911
+ if (!file) return `file not found: ${input.entity_name}`;
912
+ await file.set_role(entityValue.min_role_read);
913
+ return "Done";
914
+ }
900
915
  }
901
916
  return "Done";
902
917
  } catch (e) {
@@ -33,11 +33,7 @@ class AnyActionSkill {
33
33
  `If the task requires several independent single-step actions (e.g. "mark complete" and "mark incomplete"), call this tool once per action — do NOT bundle them into one workflow.\n\n` +
34
34
  `**Navigation is a view concern:** If a task description says "return the user to X" or "navigate back", ` +
35
35
  `do NOT add a navigation step inside the trigger. Triggers only handle data operations. ` +
36
- `Navigation (GoBack) is configured on the button in the list view, not inside the trigger itself.\n\n` +
37
- `**Verification/confirmation emails after user registration:** Create a trigger with event "Insert" on the "users" table, ` +
38
- `action_type "run_js_code". Do NOT use the send_email action — it will not work for verification. ` +
39
- `Set the action_config "code" field to exactly:\n` +
40
- `const { send_verification_email } = require("@saltcorn/data/models/email");\nawait send_verification_email(row, req);`
36
+ `Navigation (GoBack) is configured on the button in the list view, not inside the trigger itself.`
41
37
  );
42
38
  }
43
39
 
@@ -23,6 +23,21 @@ const {
23
23
  getRelationPathsForPairs,
24
24
  } = require("../relation-paths");
25
25
 
26
+ const collectViewLinks = (segment, out = []) => {
27
+ if (!segment || typeof segment !== "object") return out;
28
+ if (Array.isArray(segment)) {
29
+ segment.forEach((s) => collectViewLinks(s, out));
30
+ return out;
31
+ }
32
+ if (segment.type === "view_link" && segment.view) out.push(segment);
33
+ if (segment.above) collectViewLinks(segment.above, out);
34
+ if (segment.besides) collectViewLinks(segment.besides, out);
35
+ if (segment.contents) collectViewLinks(segment.contents, out);
36
+ if (Array.isArray(segment.tabs))
37
+ segment.tabs.forEach((t) => collectViewLinks(t?.contents, out));
38
+ return out;
39
+ };
40
+
26
41
  const collectLayoutFieldNames = (segment, out = new Set()) => {
27
42
  if (!segment || typeof segment !== "object") return out;
28
43
  if (Array.isArray(segment)) {
@@ -129,7 +144,8 @@ class GenerateViewSkill {
129
144
  `(3) Write out the complete updated configuration JSON in full — every key from the existing config must be present, with only your targeted changes merged in.\n` +
130
145
  `(4) Call apply_view_config with that complete object. NEVER call apply_view_config before step (3) is finished. NEVER call it with only the name or a partial object — the configuration field is mandatory and must be the full merged result from step (3). Calling apply_view_config without a complete configuration is an error.\n\n` +
131
146
  `**Generating a new view that contains view_links or embedded views:**\n` +
132
- `Call get_relation_paths once with all source_table/target_view pairs you need before constructing the layout.\n\n` +
147
+ `If the task or prompt mentions a viewlink, a link to another view, or a button that opens another view from a list row, that view_link column is REQUIRED — do not omit it. ` +
148
+ `You MUST call get_relation_paths with all source_table/target_view pairs before constructing the layout. Never skip this step when view_links are needed.\n\n` +
133
149
  `**Embedded view segment format (for Show layouts):**\n` +
134
150
  ` { "type": "view", "view": "<viewName>", "name": "<viewName>", "relation": "<from get_relation_paths>" }\n` +
135
151
  `Do NOT use blank text segments as placeholders — always use a real view segment with a relation string from get_relation_paths.\n\n` +
@@ -161,11 +177,16 @@ class GenerateViewSkill {
161
177
  error: `View "${name}" already exists. Use get_view_config and apply_view_config to update it.`,
162
178
  };
163
179
  const tableRow = table ? Table.findOne({ name: table }) : null;
164
- const roleName = typeof min_role === "number" ? null : (min_role || "public");
180
+ const roleName =
181
+ typeof min_role === "number" ? null : min_role || "public";
165
182
  const resolvedRole =
166
183
  typeof min_role === "number"
167
184
  ? min_role
168
- : ((getState().roles || []).find((r) => r.role === roleName) || { id: 100 }).id;
185
+ : (
186
+ (getState().roles || []).find((r) => r.role === roleName) || {
187
+ id: 100,
188
+ }
189
+ ).id;
169
190
  await View.create({
170
191
  name,
171
192
  viewtemplate: viewpattern,
@@ -308,6 +329,21 @@ class GenerateViewSkill {
308
329
  });
309
330
  if (baseCfg?.columns) wfctx.columns = baseCfg.columns;
310
331
  }
332
+ if (viewpattern === "List" && wfctx.layout) {
333
+ // initial_config_all_fields never generates ViewLink columns — inject them from layout
334
+ const viewLinks = collectViewLinks(wfctx.layout);
335
+ const viewLinkColumns = viewLinks.map((seg) => ({
336
+ type: "ViewLink",
337
+ view: seg.view,
338
+ block: seg.block || false,
339
+ label: seg.view_label || "",
340
+ minRole: seg.minRole || 100,
341
+ ...(seg.relation ? { relation: seg.relation } : {}),
342
+ isFormula: seg.isFormula || {},
343
+ }));
344
+ if (viewLinkColumns.length > 0)
345
+ wfctx.columns = [...(wfctx.columns || []), ...viewLinkColumns];
346
+ }
311
347
  if (viewpattern === "Edit" && table) {
312
348
  const layoutFieldNames = collectLayoutFieldNames(wfctx.layout);
313
349
  const fields = table.fields || [];
@@ -331,7 +367,10 @@ class GenerateViewSkill {
331
367
  }
332
368
  }
333
369
  if (usersFkColumnsToAdd.length > 0)
334
- wfctx.columns = [...(wfctx.columns || []), ...usersFkColumnsToAdd];
370
+ wfctx.columns = [
371
+ ...(wfctx.columns || []),
372
+ ...usersFkColumnsToAdd,
373
+ ];
335
374
  if (Object.keys(fixed).length > 0) wfctx.fixed = fixed;
336
375
  wfctx.destination_type = "Back to referer";
337
376
  }
@@ -398,12 +437,15 @@ class GenerateViewSkill {
398
437
  for (const field of form.fields) {
399
438
  if (prefilledFields.has(field.name)) continue;
400
439
  //TODO showIf
440
+ const isShowif = field.name.endsWith("_showif");
401
441
  properties[field.name] = {
402
442
  description:
403
443
  field.copilot_description ||
404
- `${field.label}.${
405
- field.sublabel ? ` ${field.sublabel}` : ""
406
- }`,
444
+ (isShowif
445
+ ? `${field.label}. The correct default is an empty string — leave it blank to always show this element. Only provide a JavaScript expression if the task description explicitly states that this element should be conditionally hidden based on a URL state variable or the current user. Never invent field names or copy examples.`
446
+ : `${field.label}.${
447
+ field.sublabel ? ` ${field.sublabel}` : ""
448
+ }`),
407
449
  ...fieldProperties(field),
408
450
  };
409
451
  if (!properties[field.name].type) {
@@ -44,7 +44,7 @@ const requirementsList = async (req) => {
44
44
  type: "CopilotConstructMgr",
45
45
  name: "requirement",
46
46
  },
47
- { orderBy: "written_at" },
47
+ { orderBy: "written_at" }
48
48
  );
49
49
  const starFieldview = getState().types.Integer.fieldviews.show_star_rating;
50
50
 
@@ -67,43 +67,85 @@ const requirementsList = async (req) => {
67
67
  class: "btn btn-outline-danger btn-sm",
68
68
  onclick: `view_post("${viewname}", "del_req", {id:${r.id}})`,
69
69
  },
70
- i({ class: "fas fa-trash-alt" }),
70
+ i({ class: "fas fa-trash-alt" })
71
71
  ),
72
72
  },
73
73
  ],
74
- rs,
74
+ rs
75
75
  ),
76
76
  button(
77
77
  {
78
78
  class: "btn btn-outline-danger mb-4",
79
79
  onclick: `view_post("${viewname}", "del_all_reqs")`,
80
80
  },
81
- "Delete all",
82
- ),
81
+ "Delete all"
82
+ )
83
83
  );
84
- } else {
84
+ }
85
+
86
+ const generating = await MetaData.findOne({
87
+ type: "CopilotConstructMgr",
88
+ name: "generating_requirements",
89
+ });
90
+ if (generating) {
85
91
  return div(
86
92
  { class: "mt-2" },
87
- p("No requirements found"),
88
- button(
89
- {
90
- class: "btn btn-primary",
91
- onclick: `press_store_button(this);view_post("${viewname}", "gen_reqs")`,
92
- },
93
- "Generate requirements",
93
+ p(
94
+ i({ class: "fas fa-spinner fa-spin me-2" }),
95
+ "Generating requirements, please wait..."
94
96
  ),
97
+ script(
98
+ domReady(`
99
+ (function() {
100
+ const poll = () => {
101
+ view_post(${JSON.stringify(viewname)}, 'req_status', {}, (resp) => {
102
+ if (resp && !resp.generating) location.reload();
103
+ else setTimeout(poll, 3000);
104
+ });
105
+ };
106
+ setTimeout(poll, 3000);
107
+ })();
108
+ `)
109
+ )
95
110
  );
96
111
  }
112
+
113
+ return div(
114
+ { class: "mt-2", id: "req-gen-area" },
115
+ p("No requirements found"),
116
+ button(
117
+ { class: "btn btn-primary", onclick: `copilotGenReqs()` },
118
+ "Generate requirements"
119
+ ),
120
+ script(
121
+ domReady(`
122
+ window.copilotGenReqs = () => {
123
+ document.getElementById('req-gen-area').innerHTML =
124
+ '<p><i class="fas fa-spinner fa-spin me-2"></i>Generating requirements, please wait...</p>';
125
+ view_post(${JSON.stringify(viewname)}, 'gen_reqs', {}, () => {});
126
+ const poll = () => {
127
+ view_post(${JSON.stringify(viewname)}, 'req_status', {}, (resp) => {
128
+ if (resp && !resp.generating) location.reload();
129
+ else setTimeout(poll, 3000);
130
+ });
131
+ };
132
+ setTimeout(poll, 3000);
133
+ };
134
+ `)
135
+ )
136
+ );
97
137
  };
98
138
 
99
- const gen_reqs = async (table_id, viewname, config, body, { req, res }) => {
100
- const spec = await MetaData.findOne({
139
+ const doGenReqs = async (spec, userId) => {
140
+ const generatingMd = await MetaData.create({
101
141
  type: "CopilotConstructMgr",
102
- name: "spec",
142
+ name: "generating_requirements",
143
+ body: {},
144
+ user_id: userId,
103
145
  });
104
- if (!spec) throw new Error("Specification not found");
105
- const answer = await getState().functions.llm_generate.run(
106
- `Generate the requirements for this application:
146
+ try {
147
+ const answer = await getState().functions.llm_generate.run(
148
+ `Generate the requirements for this application:
107
149
 
108
150
  Description: ${spec.body.description}
109
151
  Audience: ${spec.body.audience}
@@ -113,24 +155,44 @@ Visual style: ${spec.body.visual_style}
113
155
 
114
156
  Now use the make_requirements tool to list the requirements for this software application
115
157
  `,
116
- {
117
- tools: [requirements_tool],
118
- ...tool_choice("make_requirements"),
119
- systemPrompt:
120
- "You are a project manager. The user wants to build an application, and you must analyse their application description",
121
- },
122
- );
158
+ {
159
+ tools: [requirements_tool],
160
+ ...tool_choice("make_requirements"),
161
+ systemPrompt:
162
+ "You are a project manager. The user wants to build an application, and you must analyse their application description",
163
+ }
164
+ );
165
+ const tc = answer.getToolCalls()[0];
166
+ for (const reqm of tc.input.requirements)
167
+ await MetaData.create({
168
+ type: "CopilotConstructMgr",
169
+ name: "requirement",
170
+ body: reqm,
171
+ user_id: userId,
172
+ });
173
+ } finally {
174
+ await generatingMd.delete();
175
+ }
176
+ };
123
177
 
124
- const tc = answer.getToolCalls()[0];
178
+ const gen_reqs = async (table_id, viewname, config, body, { req, res }) => {
179
+ const spec = await MetaData.findOne({
180
+ type: "CopilotConstructMgr",
181
+ name: "spec",
182
+ });
183
+ if (!spec) throw new Error("Specification not found");
184
+ doGenReqs(spec, req.user?.id).catch((e) =>
185
+ console.error("gen_reqs error", e)
186
+ );
187
+ return { json: { success: true } };
188
+ };
125
189
 
126
- for (const reqm of tc.input.requirements)
127
- await MetaData.create({
128
- type: "CopilotConstructMgr",
129
- name: "requirement",
130
- body: reqm,
131
- user_id: req.user?.id,
132
- });
133
- return { json: { reload_page: true } };
190
+ const req_status = async (table_id, viewname, config, body, { req, res }) => {
191
+ const generating = await MetaData.findOne({
192
+ type: "CopilotConstructMgr",
193
+ name: "generating_requirements",
194
+ });
195
+ return { json: { generating: !!generating } };
134
196
  };
135
197
 
136
198
  const del_req = async (table_id, viewname, config, body, { req, res }) => {
@@ -151,6 +213,6 @@ const del_all_reqs = async (table_id, viewname, config, body, { req, res }) => {
151
213
  return { json: { reload_page: true } };
152
214
  };
153
215
 
154
- const req_routes = { gen_reqs, del_req, del_all_reqs };
216
+ const req_routes = { gen_reqs, req_status, del_req, del_all_reqs };
155
217
 
156
218
  module.exports = { requirementsList, req_routes };
@@ -43,6 +43,7 @@ const runTask = async (md_id, req) => {
43
43
  { skill_type: "Generate trigger", yoloMode: true },
44
44
  { skill_type: "Generate View", yoloMode: true },
45
45
  { skill_type: "Install Plugin", yoloMode: true },
46
+ { skill_type: "Registry editor", yoloMode: true },
46
47
  ],
47
48
  },
48
49
  });
@@ -58,49 +59,68 @@ Important: The database schema is already fully implemented. Do NOT use generate
58
59
 
59
60
  Important: Some fields are non-stored (virtual) calculated fields — they have no database column and are computed on-the-fly by Saltcorn. Never include such fields in modify_row, SQL UPDATE statements, or recalculate_stored_fields calls. Only fields that exist as actual database columns (regular fields and stored calculated fields) can be written. If a calculated field needs updating, it will refresh automatically when the fields it depends on change.
60
61
 
61
- Important: The "users" table is built-in. Passwords are platform-managed — never add a password field to a view. Signup uses a built-in form, not an Edit view.
62
+ Important: The "users" table is built-in. Passwords are platform-managed — never add a password field to a view. Signup uses the built-in page at /auth/signup, login at /auth/login. Do NOT create triggers for registration or email verification — the platform handles this natively.
63
+
64
+ Important: On landing pages, place Log in / Create account buttons in no more than two locations (e.g. navbar and one hero call-to-action). Do not repeat them in a third "Get started" section or anywhere else. For links that take an already-authenticated user to their dashboard, use href="/" — not /auth/login.
65
+
66
+ Important: Do not name any page or view "Admin dashboard" — that name is reserved by the Saltcorn platform. For pages intended for role 1 (admin), use a name like "App admin dashboard" or prefix it with the application name (e.g. "Law Firm admin dashboard").
67
+
68
+ Important: When creating a page or view, always set min_role based on the intended audience: 1 for admin-only, 40 for staff and above, 80 for logged-in users and above, 100 for public. Never default to public (100) unless the page or view is explicitly intended for unauthenticated users (e.g. a landing page). A dashboard or view for clients/users is role 80, a staff page or view is role 40, an admin page or view is role 1.
69
+
70
+ Important: Two-factor authentication (2FA/TOTP) is fully built into the platform. To configure it, call set_entity directly with entity_type "system-configuration-value" and entity_name "twofa_policy_by_role". The entity_definition must be the plain JSON object itself — for example: {"1": "Mandatory", "100": "Disabled"}. Do NOT wrap it in {"type": "json", "value": ...} or any other envelope. Read the current value first with get_entity and merge rather than overwrite. Do NOT create a workflow or trigger to do this.
71
+
72
+ Important: To set a page as the home page for a role, call set_entity directly with entity_type "system-configuration-value" and entity_name "home_page_by_role". The value is a JSON object mapping role IDs to page names — Role IDs: public=100, user=80, staff=40, admin=1. The entity_definition must be the plain JSON object itself — for example: {"100": "landing", "80": "client_dashboard"}. Do NOT wrap it in {"type": "json", "value": ...} or any other envelope. Read the current value first with get_entity so you can merge rather than overwrite. Do NOT create a workflow or trigger to do this — use set_entity directly.
73
+
74
+ Important: If the task description mentions adding a viewlink, linking rows to another view, or a button that opens another view from a list — that viewlink column MUST be present in the finished view. Do not skip it. Viewlinks require calling get_relation_paths first to obtain the relation string before generating the layout.
75
+
76
+ Important: Before creating or updating any view or page that embeds, links to, or opens another view (including viewlinks, action buttons, and ajax_modal calls), call list_entities (entity_type "view") to get all existing view names. Only reference views that appear in that list — never invent a name or assume a view exists. If a view is not in the list, omit the reference entirely. Do the same for pages: call list_entities (entity_type "page") before linking to any page by name.
62
77
 
63
78
  Your task now is:
64
79
  ${md.body.description}`;
65
- const safeReq = req?.__
66
- ? req
67
- : { ...req, __: (s) => s, user: req?.user };
80
+ const safeReq = req?.__ ? req : { ...req, __: (s) => s, user: req?.user };
68
81
 
69
82
  await md.update({ body: { ...md.body, status: "Running" } });
70
- const actionres = await agent_action.runWithoutRow({
71
- row: { prompt },
72
- req: safeReq,
73
- user: safeReq.user,
74
- });
75
- const run_id = actionres.json.run_id;
76
- const run = await WorkflowRun.findOne({ id: run_id });
77
- await agent_action.runWithoutRow({
78
- row: {
79
- prompt:
80
- "Write a description of what you did, for the purposes of a progress report. Write 1-4 sentences. Do not use any tools or write any code",
81
- },
82
- req: safeReq,
83
- run,
84
- user: safeReq.user,
85
- });
86
- const lastInteraction =
87
- run.context.interactions[run.context.interactions.length - 1];
88
- const lastText =
89
- typeof lastInteraction.content === "string"
90
- ? lastInteraction.content
83
+ try {
84
+ const actionres = await agent_action.runWithoutRow({
85
+ row: { prompt },
86
+ req: safeReq,
87
+ user: safeReq.user,
88
+ });
89
+ const run_id = actionres.json.run_id;
90
+ const run = await WorkflowRun.findOne({ id: run_id });
91
+ await agent_action.runWithoutRow({
92
+ row: {
93
+ prompt:
94
+ "Write a description of what you did, for the purposes of a progress report. Write 1-4 sentences. Do not use any tools or write any code",
95
+ },
96
+ req: safeReq,
97
+ run,
98
+ user: safeReq.user,
99
+ });
100
+ const updatedRun = await WorkflowRun.findOne({ id: run_id });
101
+ const lastInteraction =
102
+ updatedRun.context.interactions[
103
+ updatedRun.context.interactions.length - 1
104
+ ];
105
+ const lastText =
106
+ typeof lastInteraction.content === "string"
107
+ ? lastInteraction.content
91
108
  : lastInteraction.content.text
92
109
  ? lastInteraction.content.text
93
- : Array.isArray(lastInteraction.content)
94
- ? lastInteraction.content[0].text
95
- : lastInteraction.content;
96
- await MetaData.create({
97
- type: "CopilotConstructMgr",
98
- name: "progress",
99
- body: { text: lastText, run_id, task_id: md.id },
100
- user_id: req?.user?.id,
101
- });
102
-
103
- await md.update({ body: { ...md.body, status: "Done", run_id } });
110
+ : Array.isArray(lastInteraction.content)
111
+ ? lastInteraction.content[0].text
112
+ : lastInteraction.content;
113
+ await MetaData.create({
114
+ type: "CopilotConstructMgr",
115
+ name: "progress",
116
+ body: { text: lastText, run_id, task_id: md.id },
117
+ user_id: req?.user?.id,
118
+ });
119
+ await md.update({ body: { ...md.body, status: "Done", run_id } });
120
+ } catch (e) {
121
+ await md.update({ body: { ...md.body, status: "To do" } });
122
+ throw e;
123
+ }
104
124
  };
105
125
 
106
126
  /**
@@ -128,9 +148,12 @@ const runNextTask = async (once = false) => {
128
148
  );
129
149
  const done = tasks.filter((t) => t.body.status === "Done");
130
150
  const done_names = new Set(done.map((t) => t.body.name));
151
+ const all_task_names = new Set(tasks.map((t) => t.body.name).filter(Boolean));
131
152
 
132
153
  const startable = todos.filter((t) =>
133
- t.body.depends_on.every((nm) => done_names.has(nm))
154
+ (t.body.depends_on || []).every(
155
+ (nm) => done_names.has(nm) || !all_task_names.has(nm)
156
+ )
134
157
  );
135
158
 
136
159
  if (startable[0]) {
@@ -140,6 +163,13 @@ const runNextTask = async (once = false) => {
140
163
  : null;
141
164
  await runTask(startable[0].id, { user: taskUser, __: (s) => s });
142
165
  if (!once) await runNextTask();
166
+ } else if (!once) {
167
+ const settings = await MetaData.findOne({
168
+ type: "CopilotConstructMgr",
169
+ name: "settings",
170
+ });
171
+ if (settings?.body?.running)
172
+ await settings.update({ body: { ...settings.body, running: false } });
143
173
  }
144
174
  };
145
175