fix(agiloft): repoint the block at the alrest surface and fix EWLogin by waleedlatif1 · Pull Request #6562 · simstudioai/sim · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
fix(agiloft): stop retrying refusals, and expose the outputs the new …
…operations return

Five findings from review that had gone unanswered.

An Agiloft refusal was surfacing as HTTP 500. readAlrestJson throws when the
envelope reports success:false, the route catch mapped that to 500, and the
tool runner retries 500s — so a create the server had already rejected could be
retried and duplicate the record. Refusals now return a settled failure with the
message intact; genuine faults still 500.

list_tables could not run in its primary mode. EWTable is knowledge-base scoped,
but some instances reject EWLogin without a $table, so whole-knowledge-base
discovery failed at login with nothing to fall back to. It now says what the
caller can do about it rather than surfacing the raw login error.

Upsert corrupted structured values. Every field went through String(), so a
multi-value field collapsed into one joined string instead of the documented
repeated key/value pairs, and an object silently wrote "[object Object]" into
the record. Arrays now encode as repeated pairs and objects are refused, since
Agiloft documents no encoding for them.

Two outputs were invisible in the editor. `records` was conditioned on
search_records alone, so natural language search results could not be chained,
and `callbackId` on run_action_button alone, so a queued upsert's callback could
not be wired into Async Status even though both values exist at runtime.
  • Loading branch information
waleedlatif1 committed Aug 11, 2026
commit 0aba13d76deed103db6ff4c6bd87ae2ccbb57736
78 changes: 78 additions & 0 deletions apps/sim/app/api/tools/agiloft/create_record/route.test.ts
19 changes: 18 additions & 1 deletion apps/sim/app/api/tools/agiloft/create_record/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftRecordResponse } from '@/tools/agiloft/types'
import { alrestRecordCollectionUrl } from '@/tools/agiloft/utils'
import { executeAlrestRequest, readAlrestJson } from '@/tools/agiloft/utils.server'
import {
executeAlrestRequest,
isAgiloftRefusal,
readAlrestJson,
} from '@/tools/agiloft/utils.server'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -98,6 +102,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

return NextResponse.json(result)
} catch (error) {
/**
* A refusal Agiloft already decided on is a final answer, not a transient
* fault — returning 500 would make the tool runner retry it.
*/
if (isAgiloftRefusal(error)) {
logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message })
return NextResponse.json({
success: false,
output: { id: null, fields: {} },
error: error.message,
})
}

logger.error(`[${requestId}] Error creating Agiloft record:`, error)

return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
Expand Down
19 changes: 18 additions & 1 deletion apps/sim/app/api/tools/agiloft/delete_record/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftDeleteResponse } from '@/tools/agiloft/types'
import { alrestDeleteRecordUrl } from '@/tools/agiloft/utils'
import { executeAlrestRequest, readAlrestJson } from '@/tools/agiloft/utils.server'
import {
executeAlrestRequest,
isAgiloftRefusal,
readAlrestJson,
} from '@/tools/agiloft/utils.server'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -73,6 +77,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

return NextResponse.json(result)
} catch (error) {
/**
* A refusal Agiloft already decided on is a final answer, not a transient
* fault — returning 500 would make the tool runner retry it.
*/
if (isAgiloftRefusal(error)) {
logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message })
return NextResponse.json({
success: false,
output: { id: '', deleted: false },
error: error.message,
})
}

logger.error(`[${requestId}] Error deleting Agiloft record:`, error)

return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
Expand Down
47 changes: 42 additions & 5 deletions apps/sim/app/api/tools/agiloft/list_tables/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftListTablesResponse, AgiloftTableField } from '@/tools/agiloft/types'
import type {
AgiloftListTablesParams,
AgiloftListTablesResponse,
AgiloftTableField,
} from '@/tools/agiloft/types'
import { buildListTablesUrl } from '@/tools/agiloft/utils'
import { executeAgiloftRequest, readAlrestJson } from '@/tools/agiloft/utils.server'
import {
executeAgiloftRequest,
isAgiloftRefusal,
readAlrestJson,
} from '@/tools/agiloft/utils.server'

export const dynamic = 'force-dynamic'

Expand All @@ -34,6 +42,7 @@ interface EwTableResult {

export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
let params: AgiloftListTablesParams | undefined

try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
Expand Down Expand Up @@ -65,13 +74,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
const listParams = parsed.data.body
params = listParams

/** EWTable must run under EWLogin or OAuth authorization. */
const result = await executeAgiloftRequest<AgiloftListTablesResponse>(
params,
listParams,
(base) => ({
url: buildListTablesUrl(base, params),
url: buildListTablesUrl(base, listParams),
method: 'GET',
headers: { Accept: 'application/json' },
}),
Expand Down Expand Up @@ -108,6 +118,33 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

return NextResponse.json(result)
} catch (error) {
/**
* A refusal Agiloft already decided on is a final answer, not a transient
* fault — returning 500 would make the tool runner retry it.
*/
/**
* EWTable is knowledge-base scoped, but some instances reject EWLogin
* without a $table. When that happens there is nothing to fall back to, so
* say what the caller can actually do about it.
*/
if (!params?.table && /\$table/.test(toError(error).message)) {
return NextResponse.json({
success: false,
output: { tables: [], totalCount: 0 },
error:
'This Agiloft instance requires a table name to authenticate. Put any known table in the Table field — it also narrows the result to that table.',
})
}

if (isAgiloftRefusal(error)) {
logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message })
return NextResponse.json({
success: false,
output: { tables: [], totalCount: 0 },
error: error.message,
})
}

logger.error(`[${requestId}] Error listing Agiloft tables:`, error)

return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
Expand Down
14 changes: 14 additions & 0 deletions apps/sim/app/api/tools/agiloft/read_record/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { alrestRecordUrl, alrestSearchUrl, parseFieldList } from '@/tools/agilof
import {
type AgiloftRequestConfig,
executeAlrestRequest,
isAgiloftRefusal,
readAlrestJson,
} from '@/tools/agiloft/utils.server'

Expand Down Expand Up @@ -138,6 +139,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

return NextResponse.json(result)
} catch (error) {
/**
* A refusal Agiloft already decided on is a final answer, not a transient
* fault — returning 500 would make the tool runner retry it.
*/
if (isAgiloftRefusal(error)) {
logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message })
return NextResponse.json({
success: false,
output: { id: null, fields: {} },
error: error.message,
})
}

logger.error(`[${requestId}] Error reading Agiloft record:`, error)

return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
Expand Down
19 changes: 18 additions & 1 deletion apps/sim/app/api/tools/agiloft/saved_search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftSavedSearchResponse } from '@/tools/agiloft/types'
import { buildSavedSearchUrl } from '@/tools/agiloft/utils'
import { executeAgiloftRequest, readAlrestJson } from '@/tools/agiloft/utils.server'
import {
executeAgiloftRequest,
isAgiloftRefusal,
readAlrestJson,
} from '@/tools/agiloft/utils.server'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -84,6 +88,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

return NextResponse.json(result)
} catch (error) {
/**
* A refusal Agiloft already decided on is a final answer, not a transient
* fault — returning 500 would make the tool runner retry it.
*/
if (isAgiloftRefusal(error)) {
logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message })
return NextResponse.json({
success: false,
output: { searches: [], totalCount: 0 },
error: error.message,
})
}

logger.error(`[${requestId}] Error listing Agiloft saved searches:`, error)

return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
Expand Down
19 changes: 18 additions & 1 deletion apps/sim/app/api/tools/agiloft/search_records/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftSearchResponse } from '@/tools/agiloft/types'
import { AGILOFT_MAX_SEARCH_RECORDS, alrestSearchUrl, parseFieldList } from '@/tools/agiloft/utils'
import { executeAlrestRequest, readAlrestJson } from '@/tools/agiloft/utils.server'
import {
executeAlrestRequest,
isAgiloftRefusal,
readAlrestJson,
} from '@/tools/agiloft/utils.server'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -95,6 +99,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

return NextResponse.json(result)
} catch (error) {
/**
* A refusal Agiloft already decided on is a final answer, not a transient
* fault — returning 500 would make the tool runner retry it.
*/
if (isAgiloftRefusal(error)) {
logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message })
return NextResponse.json({
success: false,
output: { records: [], totalCount: 0, page: 0, limit: 0, truncated: false },
error: error.message,
})
}

logger.error(`[${requestId}] Error searching Agiloft records:`, error)

return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
Expand Down
19 changes: 18 additions & 1 deletion apps/sim/app/api/tools/agiloft/update_record/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftRecordResponse } from '@/tools/agiloft/types'
import { alrestRecordUrl } from '@/tools/agiloft/utils'
import { executeAlrestRequest, readAlrestJson } from '@/tools/agiloft/utils.server'
import {
executeAlrestRequest,
isAgiloftRefusal,
readAlrestJson,
} from '@/tools/agiloft/utils.server'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -85,6 +89,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

return NextResponse.json(result)
} catch (error) {
/**
* A refusal Agiloft already decided on is a final answer, not a transient
* fault — returning 500 would make the tool runner retry it.
*/
if (isAgiloftRefusal(error)) {
logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message })
return NextResponse.json({
success: false,
output: { id: null, fields: {} },
error: error.message,
})
}

logger.error(`[${requestId}] Error updating Agiloft record:`, error)

return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
Expand Down
13 changes: 12 additions & 1 deletion apps/sim/app/api/tools/agiloft/upsert_record/route.ts
Loading
Loading