Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ module.exports = (env, argv) => {
},
};

const isE2E = process.env.ORGII_E2E === "1";
const isE2E = process.env.ORGII_E2E === "1" || process.env.WEBDRIVER === "1";
const devServerPort = Number.parseInt(
process.env.WEBPACK_DEV_SERVER_PORT ?? process.env.PORT ?? "1998",
10
Expand Down Expand Up @@ -647,6 +647,10 @@ module.exports = (env, argv) => {
// Inline-compared in src/index.tsx so webpack constant-folds the
// `webpackMode: "eager"` App import away on platforms that don't need it.
"process.env.ORGII_DEV_EAGER_APP": JSON.stringify(String(eagerDevApp)),
// WebDriver builds are explicit test artifacts, even when their
// embedded frontend uses production optimization. Ordinary release
// builds receive "0", so E2E helpers remain tree-shaken away.
"process.env.ORGII_E2E": JSON.stringify(isE2E ? "1" : "0"),
// Local Rust IDE-server port, baked into the bundle so a second app
// instance (dual-instance collab testing) talks to its own backend.
// Must match the ORGII_IDE_SERVER_PORT the Rust side is launched with.
Expand All @@ -657,7 +661,7 @@ module.exports = (env, argv) => {
process.env.ORGII_DEEP_LINK_SCHEME ?? "orgii"
),
"process.env.ORGII_AGENT_ORG_REDESIGN": JSON.stringify(
process.env.ORGII_AGENT_ORG_REDESIGN ?? "0"
isE2E ? "1" : (process.env.ORGII_AGENT_ORG_REDESIGN ?? "0")
),
"process.env.E2E_BASE_URL": JSON.stringify(
process.env.E2E_BASE_URL ??
Expand Down
41 changes: 41 additions & 0 deletions scripts/dev/webpack-config-light.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ function withEnv(overrides, fn) {
}
}

function getDefinedValue(config, key) {
const definePlugin = config.plugins.find(
(plugin) => plugin.constructor?.name === "DefinePlugin"
);
return definePlugin?.definitions?.[key];
}

test("light dev disables webpack dev-server browser client", () => {
const config = withEnv(
{
Expand Down Expand Up @@ -76,3 +83,37 @@ test("production keeps default HTML script injection", () => {
assert.equal(htmlPlugin?.userOptions?.inject, "body");
assert.equal(htmlPlugin?.userOptions?.retryMainScriptLoad, false);
});

test("WebDriver production bundles enable the E2E-only Agent Org gate", () => {
const config = withEnv(
{
ORGII_E2E: null,
ORGII_AGENT_ORG_REDESIGN: null,
WEBDRIVER: "1",
},
() => createWebpackConfig({}, { mode: "production" })
);

assert.equal(getDefinedValue(config, "process.env.ORGII_E2E"), '"1"');
assert.equal(
getDefinedValue(config, "process.env.ORGII_AGENT_ORG_REDESIGN"),
'"1"'
);
});

test("ordinary production bundles keep the Agent Org rollout disabled", () => {
const config = withEnv(
{
ORGII_E2E: null,
ORGII_AGENT_ORG_REDESIGN: null,
WEBDRIVER: null,
},
() => createWebpackConfig({}, { mode: "production" })
);

assert.equal(getDefinedValue(config, "process.env.ORGII_E2E"), '"0"');
assert.equal(
getDefinedValue(config, "process.env.ORGII_AGENT_ORG_REDESIGN"),
'"0"'
);
});
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ libc = "0.2"
[features]
default = []
# E2E WebDriver automation plugin (debug/test only). Enable with --features webdriver.
webdriver = ["dep:tauri-plugin-webdriver-automation"]
webdriver = ["dep:tauri-plugin-webdriver-automation", "agent_core/webdriver"]
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }

Expand Down
3 changes: 3 additions & 0 deletions src-tauri/crates/agent-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ path = "src/lib.rs"
[features]
default = []
wingman-bar-native = []
# Explicit test artifact capability forwarded by the desktop `webdriver`
# feature. Ordinary release builds do not enable this.
webdriver = []
# ---------------------------------------------------------------------------
# Workspace dependencies
# ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,90 @@ use super::record::{row_to_record, AgentInboxBatch};
use super::{AgentInboxStore, MAX_INBOX_DRAIN_PAYLOAD_BYTES, MAX_INBOX_DRAIN_ROWS};

impl AgentInboxStore {
/// Load the single formal Inbox input bound to one persisted
/// `TaskExecution` context.
///
/// A generic member drain is intentionally too broad for a task-bound Turn: it could
/// acknowledge another Task's assignment or a user-directed message in
/// the same provider Turn. Pending Tasks consume their oldest matching
/// `TaskAssigned`; an in-progress Plan Task consumes its oldest matching
/// changes-requested approval response. The Task row and approval mapping
/// remain authoritative, while the returned source row stays unread until
/// the normal deferred guard commits after a successful Turn.
pub fn list_unread_task_input_for_member(
recipient_member_id: &str,
org_run_id: &str,
task_id: &str,
) -> Result<AgentInboxBatch, String> {
let conn = get_connection().map_err(|err| err.to_string())?;
let mut stmt = conn
.prepare(
"SELECT inbox.id,
inbox.recipient_agent_id,
inbox.recipient_member_id,
inbox.sender_agent_id,
inbox.sender_member_id,
inbox.org_run_id,
inbox.payload_kind,
inbox.payload_json,
inbox.request_id,
inbox.created_at,
inbox.read_at
FROM agent_org_runtime_inbox inbox
JOIN agent_org_runtime_tasks task
ON task.org_run_id=inbox.org_run_id
AND task.id=?3
AND task.owner=?1
WHERE inbox.recipient_member_id=?1
AND inbox.org_run_id=?2
AND inbox.read_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution
WHERE resolution.inbox_id=inbox.id
)
AND (
(task.status='pending'
AND inbox.payload_kind='task_assigned'
AND json_valid(inbox.payload_json)
AND json_type(inbox.payload_json,'$.task_id')='text'
AND json_extract(inbox.payload_json,'$.task_id')=?3)
OR
(task.status='in_progress'
AND task.execution_mode='plan'
AND inbox.payload_kind='plan_approval_response'
AND json_valid(inbox.payload_json)
AND json_type(inbox.payload_json,'$.request_id')='text'
AND EXISTS (
SELECT 1
FROM agent_org_runtime_plan_approvals approval
WHERE approval.org_run_id=?2
AND approval.source_task_id=?3
AND approval.source_member_id=?1
AND approval.status='changes_requested'
AND approval.request_id=json_extract(
inbox.payload_json,'$.request_id'
)
))
)
ORDER BY inbox.id ASC
LIMIT 2",
)
.map_err(|err| err.to_string())?;
let rows = stmt
.query_map(
params![recipient_member_id, org_run_id, task_id],
row_to_record,
)
.map_err(|err| err.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|err| err.to_string())?;
let has_more = rows.len() > 1;
Ok(AgentInboxBatch {
rows: rows.into_iter().take(1).collect(),
has_more,
})
}

/// `EXISTS`-style unread probe. Periodic scanners (watchdog) only
/// need the boolean; loading and decoding full rows for it is
/// wasted work.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -952,14 +952,16 @@ impl AgentInboxStore {
.query_map(
params![
org_run_id,
(crate::coordination::agent_org_payload_limits::TASK_RUN_MAX_TASKS + 1) as i64,
(crate::coordination::agent_org_payload_limits::TASK_RUN_MAX_OPEN_TASKS + 1)
as i64,
],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
)
.map_err(|err| err.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|err| err.to_string())?;
if open_tasks.len() > crate::coordination::agent_org_payload_limits::TASK_RUN_MAX_TASKS {
if open_tasks.len() > crate::coordination::agent_org_payload_limits::TASK_RUN_MAX_OPEN_TASKS
{
return Err(
"Agent Org task board exceeds the supported assignment snapshot limit".to_string(),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,12 +402,24 @@ fn open_assignment_snapshot_uses_current_tasks_and_expression_index() {
let run_id = format!("run-{}", uuid::Uuid::new_v4());
let now = chrono::Utc::now().to_rfc3339();
for (task_id, status) in [("open-task", "pending"), ("done-task", "completed")] {
let output_json = (status == "completed").then(|| {
serde_json::json!({
"summary": "done",
"content": null,
"artifactIds": [],
"producedByMemberId": "member-worker",
"producedAt": &now,
})
.to_string()
});
conn.execute(
"INSERT INTO agent_org_runtime_tasks
(id, org_run_id, subject, description, status, owner,
blocks_json, blocked_by_json, created_at, updated_at)
VALUES (?1, ?2, ?1, '', ?3, 'member-worker', '[]', '[]', ?4, ?4)",
params![task_id, &run_id, status, &now],
execution_mode, blocked_by_json, output_json,
created_by_participant_id, source_turn_intent_id, created_at, updated_at)
VALUES (?1, ?2, ?1, '', ?3, 'member-worker', 'build', '[]', ?4,
'coordinator', 'test-turn', ?5, ?5)",
params![task_id, &run_id, status, output_json, &now],
)
.expect("seed task");
AgentInboxStore::insert(InsertInboxParams {
Expand Down Expand Up @@ -460,6 +472,83 @@ fn open_assignment_snapshot_uses_current_tasks_and_expression_index() {
);
}

#[test]
fn task_execution_drain_claims_exactly_one_bound_assignment() {
let _sandbox = sandbox_with_inbox_schema();
let conn = get_connection().expect("test database");
crate::coordination::agent_org_tasks::init_schema(&conn).expect("task schema");
crate::coordination::agent_org_plan_approvals::init_schema(&conn)
.expect("plan approval schema");
let run_id = format!("run-{}", uuid::Uuid::new_v4());
let now = chrono::Utc::now().to_rfc3339();
for task_id in ["task-one", "task-two"] {
conn.execute(
"INSERT INTO agent_org_runtime_tasks
(id, org_run_id, subject, description, status, owner,
execution_mode, blocked_by_json, created_by_participant_id,
source_turn_intent_id, created_at, updated_at)
VALUES (?1,?2,?1,'','pending','member-worker','build','[]',
'coordinator','turn-create',?3,?3)",
params![task_id, &run_id, &now],
)
.expect("seed pending task");
}
let first = AgentInboxStore::insert(InsertInboxParams {
recipient_agent_id: "worker".into(),
recipient_member_id: Some("member-worker".into()),
sender_agent_id: "coordinator".into(),
sender_member_id: Some("coordinator".into()),
org_run_id: Some(run_id.clone()),
message: AgentMessage::TaskAssigned {
task_id: "task-one".into(),
subject: "Task one".into(),
description: String::new(),
assigned_by: "Coordinator".into(),
dependency_outputs: Vec::new(),
execution_mode: crate::coordination::agent_org_tasks::TaskExecutionMode::Build,
},
})
.expect("first assignment");
let second = AgentInboxStore::insert(InsertInboxParams {
recipient_agent_id: "worker".into(),
recipient_member_id: Some("member-worker".into()),
sender_agent_id: "coordinator".into(),
sender_member_id: Some("coordinator".into()),
org_run_id: Some(run_id.clone()),
message: AgentMessage::TaskAssigned {
task_id: "task-two".into(),
subject: "Task two".into(),
description: String::new(),
assigned_by: "Coordinator".into(),
dependency_outputs: Vec::new(),
execution_mode: crate::coordination::agent_org_tasks::TaskExecutionMode::Build,
},
})
.expect("second assignment");

let task_one =
AgentInboxStore::list_unread_task_input_for_member("member-worker", &run_id, "task-one")
.expect("bound task-one input");
assert_eq!(task_one.rows.len(), 1);
assert_eq!(task_one.rows[0].id, first.id);
assert!(!task_one.has_more);

let task_two =
AgentInboxStore::list_unread_task_input_for_member("member-worker", &run_id, "task-two")
.expect("bound task-two input");
assert_eq!(task_two.rows.len(), 1);
assert_eq!(task_two.rows[0].id, second.id);
assert!(!task_two.has_more);

assert_eq!(
AgentInboxStore::list_unread_for_member("member-worker", &run_id)
.expect("unread rows remain deferred")
.len(),
2,
"reading one TaskExecution input must not acknowledge either assignment"
);
}

#[test]
fn assignment_snapshot_requires_current_owner_and_valid_typed_payload() {
let _sandbox = sandbox_with_inbox_schema();
Expand All @@ -471,8 +560,10 @@ fn assignment_snapshot_requires_current_owner_and_valid_typed_payload() {
conn.execute(
"INSERT INTO agent_org_runtime_tasks
(id, org_run_id, subject, description, status, owner,
blocks_json, blocked_by_json, created_at, updated_at)
VALUES (?1, ?2, ?1, '', 'pending', 'member-b', '[]', '[]', ?3, ?3)",
execution_mode, blocked_by_json,
created_by_participant_id, source_turn_intent_id, created_at, updated_at)
VALUES (?1, ?2, ?1, '', 'pending', 'member-b', 'build', '[]',
'coordinator', 'test-turn', ?3, ?3)",
params![task_id, &run_id, &now],
)
.expect("seed reassigned task");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub const TASK_SUMMARY_DEPENDENCY_PREVIEW_MAX_COUNT: usize = 8;
pub const TASK_SUMMARY_ELIGIBILITY_PREVIEW_MAX_COUNT: usize = 16;
pub const TASK_SUMMARY_ARTIFACT_PREVIEW_MAX_COUNT: usize = 16;
pub const TASK_SUMMARY_PAGE_MAX_BYTES: usize = 512 * 1024;
pub const TASK_ANNOTATION_PAGE_MAX_BYTES: usize = 512 * 1024;
pub const TASK_OPEN_ID_PREVIEW_MAX_BYTES: usize = 16 * 1024;
pub const TASK_ACTIVE_FORM_MAX_CHARS: usize = 1_000;
pub const TASK_ACTIVE_FORM_MAX_BYTES: usize = TASK_ACTIVE_FORM_MAX_CHARS * 4;
Expand All @@ -40,12 +41,12 @@ pub const TASK_ELIGIBILITY_TOTAL_MAX_BYTES: usize = TASK_ELIGIBILITY_TOTAL_MAX_C
pub const TASK_DEPENDENCY_JSON_MAX_BYTES: usize = 256 * 1024;
pub const RFC3339_TIMESTAMP_MAX_CHARS: usize = 64;
pub const RFC3339_TIMESTAMP_MAX_BYTES: usize = RFC3339_TIMESTAMP_MAX_CHARS * 4;
/// Maximum number of durable task rows that one Agent Org run may retain.
/// This is a run-level storage boundary, not a recommendation for how many
/// tasks a coordinator should create in one model tool call.
pub const TASK_RUN_MAX_TASKS: usize = 200;
/// Maximum number of open (`pending`/`in_progress`) Tasks one long-lived Team
/// may retain at once. Terminal history is intentionally not counted: it is
/// durable and cursor-paged rather than eventually preventing new work.
pub const TASK_RUN_MAX_OPEN_TASKS: usize = 200;
/// LLM-facing request limit for one atomic `task_graph_create` call. Keeping
/// this separate from [`TASK_RUN_MAX_TASKS`] avoids teaching coordinators to
/// this separate from [`TASK_RUN_MAX_OPEN_TASKS`] avoids teaching coordinators to
/// create 200-node graphs merely because the database can retain that many.
pub const TASK_GRAPH_CREATE_MAX_TASKS: usize = 32;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub struct AgentOrgPlanApproval {
pub source_task_id: String,
pub source_member_id: String,
pub source_session_id: String,
pub source_turn_intent_id: String,
pub root_session_id: String,
pub policy: PlanApprovalPolicy,
pub status: AgentOrgPlanApprovalStatus,
Expand Down Expand Up @@ -109,6 +110,7 @@ pub struct AgentOrgPlanApprovalSummary {
pub source_task_id: String,
pub source_member_id: String,
pub source_session_id: String,
pub source_turn_intent_id: String,
pub root_session_id: String,
pub policy: PlanApprovalPolicy,
pub status: AgentOrgPlanApprovalStatus,
Expand All @@ -124,6 +126,7 @@ pub struct CreateAgentOrgPlanApprovalParams {
pub source_task_id: String,
pub source_member_id: String,
pub source_session_id: String,
pub source_turn_intent_id: String,
pub root_session_id: String,
pub policy: PlanApprovalPolicy,
pub plan_title: String,
Expand Down Expand Up @@ -162,6 +165,7 @@ pub(crate) fn create_schema(conn: &Connection) -> rusqlite::Result<()> {
source_task_id TEXT NOT NULL,
source_member_id TEXT NOT NULL,
source_session_id TEXT NOT NULL,
source_turn_intent_id TEXT NOT NULL,
root_session_id TEXT NOT NULL,
policy TEXT NOT NULL,
status TEXT NOT NULL,
Expand Down
Loading
Loading